FlutterでTimerアプリを作成する

Table of Contents

Table of Contents

今回はFlutterで簡単なTimerアプリを作成してみたくて、その経過、結果、勉強したものを記録します。



アプリの要望

アプリの機能要望

  • [O] カウントダウンの開始
  • [O] カウントダウン時間表示
  • カウントダウン時間の設定
  • カウントダウン終了時のアラーム
  • カウントダウンの循環機能

アプリの設計要望

  • デザインパターンにProviderを導入する
  • シンプルな操作方法

アプリの画面



環境とライブラリー

  • Flutter: v1.17
  • Dart: v2.4
  • provider: v4.1.2


Provider

DIと状態管理のパッケージです。

優れる状態管理の機能を持っているなので、最近の人気のパッケージであり、 Google I/O 2019でも推奨されています。

BLoCと比べるとより簡易な方法で状態管理ができますので、学習コストは低いです。

参考

導入

pubspec.yamlファイルにパッケージを追加する。

dependencies:
  flutter:
    sdk: flutter
  
  //...
  provider: 4.1.2

providerには、UIとビジネスロジックの分離ができるメリットがあります。

今回のTimerアプリの核心機能となるカウントダウン時間表示について、 providerで分離して管理します。

  • ビジネスロジック: カウントダウン
  • UI: 時間表示

手順としては、ビジネスロジックのクラスを作成して、Widgetに適用する。


Main 設計

class BasicTimer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("My Timer"),
      ),
      body: Container(
        child: ChangeNotifierProvider(
          create: (context) => TimerStore(),
          child: Center(
            child: Clock(),
          ),
        ),
      ),
    );
  }
}

状態管理クラスとWidgetと連携するため、ChangeNotifierProviderを使います。


ビジネスロジックのクラス: TimeStore

import 'dart:async';
import 'package:flutter/material.dart';

class TimerStore with ChangeNotifier {
  static const COUNTER_VALUE = 60; // TODO: ユーザーが設定できるように
  var count = 0;
  static const sec = const Duration(seconds: 1);
  static const ms = const Duration(milliseconds: 1);
  Timer _timer;

  startTimer() {
    if (_timer != null) {
      _timer.cancel();
    }
    count = COUNTER_VALUE;
    _timer = Timer.periodic(sec, (timer) {
      updateCounter();
    });
  }
  void updateCounter() {
    if (count > 0)
      count--;
    else
      _timer.cancel();
    notifyListeners();
  }
  @override
  void dispose() {
    super.dispose();
    _timer.cancel();
  }
}

dart:asyncTimer.periodic()関数を利用して、定時的にカウントダウンの処理を行う。

こちらの要点としては、

  • カウントダウンの数字をcount値で保持し、UI表示の時にも使われています。
  • timerのキャンセルのタイミングはカウントダウン終了と再起動時。

UI: Clock Widget

class Clock extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final countText = context.select((TimerStore store) => getCountText(store));

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text("$countText", // <-- **カウンター時間の表示**
            style: TextStyle(
              fontFamily: 'PressStart2P',
              fontSize: 36,
            )),
        SizedBox(height: 50),
        Container(
          padding: const EdgeInsets.all(30.0),
          decoration: BoxDecoration(
              shape: BoxShape.circle,
              gradient: LinearGradient(
                  colors: <Color>[
                    Colors.blue[300],
                    Colors.blue[500],
                    Colors.blue[700]
                  ]
              )
          ),
          child: FlatButton(
            onPressed: () {
              context.read<TimerStore>().startTimer();
            },
            padding: EdgeInsets.all(20),
            shape: CircleBorder(),
            child: Text("Start", style: TextStyle(
                fontFamily: 'PressStart2P',
                fontSize: 16,
                color: Colors.white
            ),),
          ),
        )
      ],
    );
  }

  String getCountText(store) {
    var hour = (store.count / (60*60)).floor();
    var mod = store.count % (60*60);
    var min = (mod / 60).floor();
    var sec = mod % 60;
    return "${twoDigits(hour)}:${twoDigits(min)}:${twoDigits(sec)}";
  }
  String twoDigits(int n) {
    if (n >= 10) {
      return "$n";
    } else {
      return "0$n";
    }
  }

}

TimeStore状態クラスにアクセスする時に、 context.select(変更を監視します)とcontext.read(変更を監視しません)を使います。

カウンター時間の表示をカウンター値によって変動するため、監視する必要がありますので、context.selectTimeStore状態クラスのcountをアクセスし、表示時間の文字を作成する。

ボタンを押してカウントダウン開始する際に、TimeStore状態クラスのstartTimer関数を実行します。 監視する必要はありませんので、context.readを使います。



感想

Providerの使用は割とシンプルです。 少ないコードで状態管理ができて素敵な〜と思いました。 また、簡単なカウントダウン機能ができましたが、まだたくさんの機能を実装していません。 もう少し頑張って、機能を充実します。