あなたの寿命がわかる余命カウントアプリハンズオン

# 完成版

![20190318_090801.GIF](https://qiita-image-store.s3.amazonaws.com/0/206255/45296b14-0fdb-e247-9554-70db12ce3c65.gif)

このアプリは自分の生年月日を入力することで、残りの寿命を知ることができるアプリです。自分に残された時間を知ることで、より今を大切に生きれるように！という思いを込めて作成しました...

Flutterの全ソースコードと解説を公開しているので、これからFlutterを学ぶ方がハンズオン形式でライトに実装できる内容になっています。

本アプリは以下の流れで作成していきます。
:zero: 事前準備
:one: レイアウト作成
:two: 日付入力と表示
:three: 残余名の計算
:four: グラフ表示

# :zero: 事前準備

公式サイトを参考に、Flutterの環境構築を行いましょう。
https://flutter.dev/docs/get-started/install

今回はMacOS/iOSエミュレータ/VSCodeという環境で実装していきますが、Windows/Android/Intellij等の組み合わせでも特に違いはありません。

VSCodeの場合、「Shift+Command+P」でNewProjectを作成できるので、今回は「lifetimer_handson」という名前でプロジェクトを作成していきます。

<img width="1376" alt="スクリーンショット 2019-03-17 17.56.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/2119f257-df7a-0eb1-4400-da9a6eafecbf.png">

エミュレータが立ち上がればOKです。もし起動に失敗する場合は、環境構築に不備がある可能性が高いので、公式サイトに戻り再度インストール手順に従って設定を行いましょう。

## サンプルプロジェクトの削除

Flutterでは、プロジェクト作成時にデフォルトでサンプル実装が記述されています。一から自分たちでアプリをつくっていく際には不要になるため、`main.dart`ファイルのソースコードを全文削除し、中身を次のように変更します。

```dart:main.dart
import 'package:flutter/material.dart';

void main() => runApp(MaterialApp(
  theme: ThemeData.dark(),
  home: Container()
));
```

<img width="1379" alt="スクリーンショット 2019-03-17 18.04.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/d8a6a8ab-d1a4-b7f6-9627-c8fba3c105ca.png">

真っ黒の画面が表示されるはずです。ここから段階的にアプリを実装してきましょう。


# :one: レイアウト作成

上記の初期実装において、`main.dart`で表示していたのは空の[Container](https://docs.flutter.io/flutter/widgets/Container-class.html)でした。今回はここに新たなページを埋め込んでいきます。

まずは新しいディレクトリとファイルを作成します。`main.dart`と同じ階層に`pages/lifetimer.dart`を作成していきましょう。

```dart:pages/lifetimer.dart
import 'package:flutter/material.dart';

class LifeTimerPage extends StatefulWidget {
  @override
  _LifeTimerPageState createState() => _LifeTimerPageState();
}

class _LifeTimerPageState extends State<LifeTimerPage> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
```

`pages/lifetimer.dart`でも空の空の[Container](https://docs.flutter.io/flutter/widgets/Container-class.html)を返していますが、今回は[StatefulWidget](https://docs.flutter.io/flutter/widgets/StatefulWidget-class.html)という状態を持つことができるクラスでラップしています。

:pencil: [Stateful or Stateless widgets?](https://flutterdoc.com/stateful-or-stateless-widgets-42a132e529ed)

このファイルを`main.dart`で使用します。importは以下のように記述することができます。

```dart:main.dart
import 'package:flutter/material.dart';
import './pages/lifetimer.dart';

void main() => runApp(MaterialApp(
  theme: ThemeData.dark(),
  home: LifeTimerPage(), // 修正
));
```

`home`として`LifeTimerPage()`を呼び出すようにしておきます。もちろんこの状態では空の[Container](https://docs.flutter.io/flutter/widgets/Container-class.html)が返るため画面は黒い状態のままです。

## 画面の大枠を作成

マテリアルデザインで基本的なレイアウトを作成するため、[Scaffold](https://docs.flutter.io/flutter/material/Scaffold-class.html)を利用して空の[Container](https://docs.flutter.io/flutter/widgets/Container-class.html)を置き換えます。

`pages/lifetimer.dart`を以下のように書き換えてください。

```dart:pages/lifetimer.dart
import 'package:flutter/material.dart';

class LifeTimerPage extends StatefulWidget {
  @override
  _LifeTimerPageState createState() => _LifeTimerPageState();
}

class _LifeTimerPageState extends State<LifeTimerPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(  // 修正
        appBar: AppBar(title: Text("LifeTimer")), 
        body: Text("LifeTimerBody"));
  }
}
```

<img width="1381" alt="スクリーンショット 2019-03-17 18.19.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/09b54cb9-a334-e1eb-351c-1f9fc0dbd0f7.png">

画面にレイアウトが表示されました。

## リスト形式による表示

また、[ListView](https://docs.flutter.io/flutter/widgets/ListView-class.html)を使用することで、テキストを縦に複数表示することが可能です。

:pencil: [Layouts in Flutter](https://flutter.dev/docs/development/ui/layout)

```dart:pages/lifetimer.dart
      body: ListView(
        children: <Widget>[
          Text("LifeTimerBody"),
          Text("LifeTimerBody"),
          Text("LifeTimerBody")
        ],
      ),
```

<img width="1384" alt="スクリーンショット 2019-03-17 18.26.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/5613923f-64e9-5caf-b3ca-dd90b64503fc.png">

ちなみに、[ListView](https://docs.flutter.io/flutter/widgets/ListView-class.html)はスクロール機能も備えているため、この画面で縦にスクロールするとにゅるにゅるとした画面スクロールを体感することができます。

さらに、[Padding](https://docs.flutter.io/flutter/widgets/Padding-class.html)でこの[ListView](https://docs.flutter.io/flutter/widgets/ListView-class.html)をラップすることにより、CSSでいうところの`padding`を追加できます。

[Padding](https://docs.flutter.io/flutter/widgets/Padding-class.html)は前述した[Container](https://docs.flutter.io/flutter/widgets/Container-class.html)でも同様の機能を実装できます。

:pencil: [Flutter — Container Cheat Sheet](https://medium.com/jlouage/container-de5b0d3ad184)

```dart:pages/lifetimer.dart
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: ListView(
          children: <Widget>[
            Text("LifeTimerBody"),
            Text("LifeTimerBody"),
            Text("LifeTimerBody")
          ],
        ),
      ),
```

<img width="1378" alt="スクリーンショット 2019-03-17 18.31.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/6eaf4907-af75-d8ec-25f1-f56e2cce0790.png">

これにより、左上の空間に少し余白が生まれました。

## タイトルの作成

画面ボディ部の一番先頭にタイトルを表示していきます。先ほど作成した`body`を以下のように置き換えます。

```dart:pages/lifetimer.dart
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: ListView(
          children: <Widget>[
            _buildTitle(), // 修正
            Text("LifeTimerBody"),
            Text("LifeTimerBody")
          ],
        ),
      ),
```

`_buildTitle()`はWidgetを返す関数です。`body`の呼び出し元と同じ`_LifeTimerPageState`クラス内で、以下の関数を作成します。

```dart:pages/lifetimer.dart

  Widget _buildTitle() {
    return Padding(
      padding: EdgeInsets.only(bottom: 10.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Icon(Icons.mood),
          SizedBox(
            width: 5.0,
          ),
          Text(
            'あなたに残された日数を計算します',
            style: TextStyle(fontSize: 18.0),
          ),
        ],
      ),
    );
  }

```

[Padding](https://docs.flutter.io/flutter/widgets/Padding-class.html)は前述した[Container](https://docs.flutter.io/flutter/widgets/Container-class.html)でラップしているのは前回と同じです。今回は`child`に [Row](https://docs.flutter.io/flutter/widgets/Row-class.html)というWidgetを使用しています。これは、複数のWidgetを横並びで配置するためです。`MainAxisAlignment.center`を指定することで、`children`に定義されている[Icon](https://docs.flutter.io/flutter/widgets/Icon-class.html)、[SizedBox](https://docs.flutter.io/flutter/widgets/SizedBox-class.html)、[Text](https://docs.flutter.io/flutter/widgets/Text-class.html)を中央揃えで配置することができます。[Text](https://docs.flutter.io/flutter/widgets/Text-class.html)では[TextStyle](https://docs.flutter.io/flutter/painting/TextStyle-class.html)を使用することでフォントサイズやフォントファミリーを指定することが可能です。

:pencil: [Flutter Layout Cheat Sheet](https://medium.com/flutter-community/flutter-layout-cheat-sheet-5363348d037e)

また、 [Padding](https://docs.flutter.io/flutter/widgets/Padding-class.html)では[EdgeInsets](https://docs.flutter.io/flutter/painting/EdgeInsets-class.html)を使用することで`padding`の指定が可能であることは前述の通りですが、今回は`only`を使用することで、`bottom`にのみ`padding`を適用しています。

<img width="1383" alt="スクリーンショット 2019-03-17 18.54.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/240de264-b3cf-f106-c2f2-bad53249a0c5.png">

実行することで、先頭のテキストが書き換わっていることを確認できます。



# :two: 日付入力と表示

Flutterで日時を入力する方法は複数存在しますが、今回は[DateTimePickerFormField](https://pub.dartlang.org/packages/datetime_picker_formfield)というライブラリを利用していきましょう。ライブラリを利用するためには`pubspec.yaml`で対象のバージョンを記述する必要があります。

```yaml:pubspec.yaml
...
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  datetime_picker_formfield: ^0.1.8 # 追加
...
```

`pubspec.yaml`で依存関係に追加したライブラリは、dartファイルから呼び出すことができるようになります。
また、日付を扱う上で欠かせないフォーマッターを利用するため、[DateFormat](https://docs.flutter.io/flutter/intl/DateFormat-class.html)を使用していきますが、これは`intl.dart`に含まれています。合わせて2つのパッケージをインポートしていきましょう。

```dart:pages/lifetimer.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:datetime_picker_formfield/datetime_picker_formfield.dart';

class LifeTimerPage extends StatefulWidget {
  @override
  _LifeTimerPageState createState() => _LifeTimerPageState();
}
...
```

今回の追加実装ではカレンダー入力操作、入力した値の保持、入力情報の画面表示を同時に記載しています。

```dart:pages/lifetimer.dart
class _LifeTimerPageState extends State<LifeTimerPage> {
  // 日時フォーマット
  final formats = {
    InputType.both: DateFormat("yyyy-MM-dd HH:mm:ss"),
    InputType.date: DateFormat('yyyy-MM-dd'),
    InputType.time: DateFormat("HH:mm"),
  };
  DateTime birthDate;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("LifeTimer")),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: ListView(
          children: <Widget>[
            _buildTitle(),
            _buildBirthDateInputField(),
            _buildBirthTextField()
          ],
        ),
      ),
    );
  }

  Widget _buildTitle() {
    return Padding(
      padding: EdgeInsets.only(bottom: 10.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Icon(Icons.mood),
          SizedBox(
            width: 5.0,
          ),
          Text(
            'あなたに残された日数を計算します',
            style: TextStyle(fontSize: 18.0),
          ),
        ],
      ),
    );
  }

  Widget _buildBirthDateInputField() {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 10.0),
      child: DateTimePickerFormField(
        inputType: InputType.date,
        format: formats[InputType.date],
        editable: true,
        decoration: InputDecoration(
            labelText: '生年月日を入力してください', hasFloatingPlaceholder: false),
        onChanged: (birthDate) => _setBirthDate(birthDate),
      ),
    );
  }

  void _setBirthDate(DateTime _birthDate) {
    setState(() {
      birthDate = _birthDate;
      print(birthDate);
    });
  }

  Widget _buildBirthTextField() {
    return Padding(
      padding: EdgeInsets.all(5.0),
      child: Text("生年月日: ${_getBirthDate()}"),
    );
  }

  String _getBirthDate() {
    return birthDate != null
        ? formats[InputType.date].format(birthDate)
        : '入力待ち';
  }
}

```

`_buildBirthDateInputField()`では、[DateTimePickerFormField](https://pub.dartlang.org/packages/datetime_picker_formfield)でカレンダーの入力を促しています。カレンダーがモーダルで表示されるライブラリとなっており、日付の指定後に`onChanged`が発火します。`onChanged`の中では`_setBirthDate`関数が呼ばれていて、`State`を継承した`_LifeTimerPageState`クラスに存在するプロパティ`birthDate`を書き換えます。この時[setState()](https://docs.flutter.io/flutter/widgets/State/setState.html)を使用しなければならないことに注意してください。

最後に、`_buildBirthTextField（）`を呼び出すことで、入力したカレンダーの日付が画面に出力されることが確認できます。

[日付入力時]
<img width="1388" alt="スクリーンショット 2019-03-17 19.13.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/4f04d5f5-4c0f-1c78-67b9-1155b573ddf3.png">

[日付入力後]
<img width="1373" alt="スクリーンショット 2019-03-17 19.14.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/976af0e3-1fc3-25ce-9287-4c2f6940d08f.png">

## 現在日時のカウントアップ

[DateTime](https://docs.flutter.io/flutter/dart-core/DateTime-class.html)と[Timer](https://docs.flutter.io/flutter/dart-async/Timer-class.html)を利用することで、現在時刻を取得し画面に表示します。[Timer](https://docs.flutter.io/flutter/dart-async/Timer-class.html)を使用するために、`dart:async`をインポートします。

```dart:pages/lifetimer.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:datetime_picker_formfield/datetime_picker_formfield.dart';
import 'dart:async'; // 追加

class LifeTimerPage extends StatefulWidget {
  @override
  _LifeTimerPageState createState() => _LifeTimerPageState();
}

class _LifeTimerPageState extends State<LifeTimerPage> {
  // 日時フォーマット
  final formats = {
    InputType.both: DateFormat("yyyy-MM-dd HH:mm:ss"),
    InputType.date: DateFormat('yyyy-MM-dd'),
    InputType.time: DateFormat("HH:mm"),
  };
  // 日時
  DateTime birthDate;
  DateTime now; // 追加

  // 追加
  @override
  void initState() {
    super.initState();
    Timer.periodic(Duration(seconds: 1), (timer) {
      setState(() {
        now = new DateTime.now();
      });
    });
  }
```

今回はWidgetが作成される時に呼ばれる`initState()`の中で、1秒ごとに現在時刻を更新する[Timer](https://docs.flutter.io/flutter/dart-async/Timer-class.html)を仕込みます。

:pencil: [Stateful Widget Lifecycle](https://flutterbyexample.com/stateful-widget-lifecycle/)

画面に現在時刻を表示するWidgetの追加実装は以下の通りです。

```dart:pages/lifetimer.dart
...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("LifeTimer")),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: ListView(
          children: <Widget>[
            _buildTitle(),
            _buildBirthDateInputField(),
            _buildBirthTextField(),
            _buildNowTextField() // 追加
          ],
        ),
      ),
    );
  }
...
  // 追加
  Widget _buildNowTextField() {
    return Padding(
      padding: EdgeInsets.all(5.0),
      child: Text("現在時刻: ${_getNow()}"),
    );
  }
  // 追加
  String _getNow() {
    return now != null ? formats[InputType.both].format(now) : "なし";
  }
```

<img width="1387" alt="スクリーンショット 2019-03-17 19.32.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/b3f4484f-f34d-bc53-9d6e-ca164e3c6654.png">

無事画面に表示されていることが確認できました。


#:three: 残余名の計算
ここからは残りの寿命を計算するロジックを追加してきます。

## 平均寿命から算出される残余命時間の計算式

日本人の平均寿命は**83.98**歳です。（2016年度調査）

厳密には異なりますが、仮に1年を365日として83年を計算すると、

83*365日=**30295日**

0.98年は**357日16時間48分**

とみなすことで、

***生まれてから30652日16時間48分後が推定命日***

と言い換えることができます。

この計算式を実装していきましょう。以前に作成した`_setBirthDate`を削除し、以下のように`_setBirthAndExpectedDeathDate`へと変更して、`birthDate`同様`expectedDeathDate`を更新します。

```dart:pages/lifetimer.dart
...
  // 日時
  DateTime birthDate;
  DateTime now;
  DateTime expectedDeathDate; // 追加
...
  Widget _buildBirthDateInputField() {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 10.0),
      child: DateTimePickerFormField(
        inputType: InputType.date,
        format: formats[InputType.date],
        editable: true,
        decoration: InputDecoration(
            labelText: '生年月日を入力してください', hasFloatingPlaceholder: false),
        onChanged: (birthDate) => _setBirthAndExpectedDeathDate(birthDate), // 修正
      ),
    );
  }

  void _setBirthAndExpectedDeathDate(DateTime _birthDate) { // 修正
    // var averageDeathAge = 83.98; // year: 83, day: 357, hour: 16, minute: 48
    var averageDaethDuration = Duration(days: 30652, hours: 16, minutes: 48);
    setState(() {
      birthDate = _birthDate;
      expectedDeathDate = birthDate.add(averageDaethDuration);
    });
  }
...
  // 追加
  Widget _buildExpectedDateTextField() {
    return Padding(
      padding: EdgeInsets.all(30.0),
      child: Column(
        children: <Widget>[
          Text("平均寿命から計算されるあなたの推定命日"),
          SizedBox(
            height: 5.0,
          ),
          Text(
            "${_getExpectedDeathDate()}",
            style: TextStyle(fontSize: 30.0, color: Colors.red[300]),
          ),
        ],
      ),
    );
  }

  // 追加
  String _getExpectedDeathDate() {
    return expectedDeathDate != null
        ? formats[InputType.date].format(expectedDeathDate)
        : "入力待ち";
  }
```

<img width="1379" alt="スクリーンショット 2019-03-17 19.52.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/ec4c01fb-3d6b-70a3-d310-2b297323f966.png">

## 寿命パラメータの表示

推定命日がわかったので、現在時刻との差分から残りの時間をカウントダウンしていきます。日単位、時間単位、分単位、秒単位でそれぞれ計算してみましょう。

```dart:pages/lifetimer.dart
...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("LifeTimer")),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: ListView(
          children: <Widget>[
            _buildTitle(),
            _buildBirthDateInputField(),
            _buildBirthTextField(),
            _buildNowTextField(),
            _buildExpectedDateTextField(),
            _buildLeftTimeParams() // 追加
          ],
        ),
      ),
    );
  }
...
  // 追加
  Widget _buildLeftTimeParams() {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.only(bottom: 10.0),
            child: Text("推定死亡日まで残り",
                style: TextStyle(decoration: TextDecoration.underline)),
          ),
          Padding(
            padding: EdgeInsets.only(left: 20.0, bottom: 3.0),
            child: Text(
              "日計算　：${_getExpectedDeathInDays()} 日",
              style: TextStyle(fontSize: 20.0),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(left: 20.0, bottom: 3.0),
            child: Text(
              "時計算　：${_getExpectedDeathInHours()} 時間",
              style: TextStyle(fontSize: 20.0),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(left: 20.0, bottom: 3.0),
            child: Text(
              "分計算　：${_getExpectedDeathInMinutes()} 分",
              style: TextStyle(fontSize: 20.0),
            ),
          ),
          Padding(
            padding: EdgeInsets.only(left: 20.0, bottom: 3.0),
            child: Text(
              "秒計算　：${_getExpectedDeathInSeconds()} 秒",
              style: TextStyle(fontSize: 20.0),
            ),
          ),
        ],
      ),
    );
  }

  // 追加
  String _getExpectedDeathInDays() {
    var leftTimeDuration = _calcLifeTimeDuration();
    return leftTimeDuration != null ? leftTimeDuration.inDays.toString() : "-";
  }
  // 追加
  String _getExpectedDeathInHours() {
    var leftTimeDuration = _calcLifeTimeDuration();
    return leftTimeDuration != null ? leftTimeDuration.inHours.toString() : "-";
  }
  // 追加
  String _getExpectedDeathInMinutes() {
    var leftTimeDuration = _calcLifeTimeDuration();
    return leftTimeDuration != null
        ? leftTimeDuration.inMinutes.toString()
        : "-";
  }
  // 追加
  String _getExpectedDeathInSeconds() {
    var leftTimeDuration = _calcLifeTimeDuration();
    return leftTimeDuration != null
        ? leftTimeDuration.inSeconds.toString()
        : "-";
  }
  // 追加
  Duration _calcLifeTimeDuration() {
    return expectedDeathDate != null ? expectedDeathDate.difference(now) : null;
  }
```

`_calcLifeTimeDuration()`では推定命日から現在時刻の差分を`Duration`として取得していて、各パラメータごとの残り時間を表示しています。

表示結果は以下のようになります。

<img width="1379" alt="スクリーンショット 2019-03-17 20.00.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/0c710105-cff0-1138-2154-77faff659630.png">

この状態で、1秒ごとに秒計算の数値がカウントダウンされるようになります。毎秒減り続ける寿命に焦る気持ちを抑えつつ実装を進めていきましょう。


# :four: グラフ表示

いよいよ大詰めです。今回は残りの寿命をパーセンテージで表していきます。

まずは表示するグラフの大きさを画面サイズから決定したいと思います。小さなデバイスでも適切なサイズで表示できるようにするためです。[MediaQuery](https://docs.flutter.io/flutter/widgets/MediaQuery-class.html)を使用して、`width`の大きさを取得し、`_buildRadialProgress`関数に値を渡します。

```dart:pages/lifetimer.dart
...
import '../widgets/lifetimer_painter.dart';
...
  @override
  Widget build(BuildContext context) {
    double width = MediaQuery.of(context).size.width; // 
    return Scaffold(
      appBar: AppBar(title: Text("LifeTimer")),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: ListView(
          children: <Widget>[
            _buildTitle(),
            _buildBirthDateInputField(),
            _buildBirthTextField(),
            _buildNowTextField(),
            _buildExpectedDateTextField(),
            _buildLeftTimeParams(),
            _buildRadialProgress(width), // 追加
          ],
        ),
      ),
    );
  }
...
   Widget _buildRadialProgress(double deviceWidth) {
    double circleSize = deviceWidth * 0.7;
    double textSize = deviceWidth * 0.065;
    return Container(
      padding: EdgeInsets.all(4.0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          CustomPaint(
            foregroundPainter: LifeTimerPainter(
                lineColor: Colors.grey,
                completeColor: Colors.blueAccent,
                completePercent: _calcLeftTimePersent(),
                width: 8.0),
            child: Container(
              padding: EdgeInsets.all(10),
              height: circleSize,
              width: circleSize,
              child: RaisedButton(
                color: Colors.blue,
                shape: CircleBorder(),
                child: Row(
                  children: <Widget>[
                    Icon(
                      Icons.directions_run,
                      size: textSize,
                    ),
                    Text(
                      "${_getLeftTimePercentStr()}",
                      style: TextStyle(fontSize: textSize),
                    ),
                  ],
                ),
                onPressed: () {},
              ),
            ),
          ),
        ],
      ),
    );
  }
...
  String _getLeftTimePercentStr() {
    if (expectedDeathDate == null) {
      return "- %";
    }
    return _calcLeftTimePersent().toStringAsFixed(8) + "%";
  }

  double _calcLeftTimePersent() {
    if (expectedDeathDate == null) {
      return 0.0;
    }
    Duration livingDuration = expectedDeathDate.difference(now);
    Duration averageDeathDuration =
        Duration(days: 30652, hours: 16, minutes: 48);
    int livingDurationInMillis = livingDuration.inMilliseconds;
    int averageDeathDurationInMillis = averageDeathDuration.inMilliseconds;
    return livingDurationInMillis * 100 / averageDeathDurationInMillis;
  }
...
```

`_buildRadialProgress`関数では[CustomPaint](https://docs.flutter.io/flutter/widgets/CustomPaint-class.html)を使用して図を描画しています。`foregroundPainter`で指定している`LifeTimerPainter`は[CustomPainter](https://docs.flutter.io/flutter/rendering/CustomPainter-class.html)を継承した自作クラスです。新たなフォルダとファイル名で新クラスを作成します。今回は`widgets/lifetimer_painter.dart`という名前にします。以下のようにソースコードを実装してください。

```dart:widgets/lifetimer_painter.dart
import 'package:flutter/material.dart';
import 'dart:math';

class LifeTimerPainter extends CustomPainter {
  Color lineColor;
  Color completeColor;
  double completePercent;
  double width;

  LifeTimerPainter(
      {this.lineColor, this.completeColor, this.completePercent, this.width});

  @override
  void paint(Canvas canvas, Size size) {
    Paint line = Paint()
      ..color = lineColor
      ..strokeCap = StrokeCap.round
      ..style = PaintingStyle.stroke
      ..strokeWidth = width;

    Paint complete = Paint()
      ..color = completeColor
      ..strokeCap = StrokeCap.round
      ..style = PaintingStyle.stroke
      ..strokeWidth = width;

    Offset center = Offset(size.width / 2, size.height / 2);
    double radius = min(size.width / 2, size.height / 2);
    canvas.drawCircle(center, radius, line);

    double arcAngle = 2 * pi * (completePercent / 100);
    canvas.drawArc(Rect.fromCircle(center: center, radius: radius), -pi / 2,
        arcAngle, false, complete);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}
```

以上で実装は完了です。最終的な画面は次のようになります。

<img width="1381" alt="スクリーンショット 2019-03-17 21.06.49.png" src="https://qiita-image-store.s3.amazonaws.com/0/206255/b9dcd8fe-46f1-dcb2-de92-ab39a95cbc70.png">

# まとめ

基本的な機能とWidgetを中心に作成してきましたが、Flutterで頻繁に登場する多くのWidgetを利用できるハンズオンになっているのではないかと思います。今回登場したWidgetには公式ページへのリンクを記載しています。Flutterは公式サイトの解説が充実しているため、これから本格的にスマホアプリを作る際には、ぜひご一読することをおすすめします。

ここまで読んでいただきありがとうございました。:bow:

ソースコードはGitHubで公開しています。
https://github.com/shunp/lifetimer_handson