FlutterでDateTimeを使ってストップウォッチアプリを作る

こんにちは、Tamappeです。
前回はストップウォッチアプリを作っていたつもりでしたが出来上がったのが時計アプリでした。
FlutterでTimerとDateTimeを使ったシンプルな時計アプリ
今回は、出直しでストップウォッチアプリを再度作ってみようと思います。
| スクリーンショット | gif動画 | 
|  |  | 
ストップウォッチの基礎的なロジックはこちらになります。
- スタートボタンをタップしたらその時の現在時刻を取得して保持する
- ポーリング処理で1秒ごとにその時の時刻を取得し、1で取得した時刻との差分を取る
- タイマー表示部分の計算ロジックを書く
- タイマーらしく00:00:00 (時:分:秒)表記の文字列に変換する
- 4で計算した文字列を画面に表示させる
この順番に解説していきますが、その前にデザインのレイアウトを組み立てていきます。
タイマー部分のウィジェットの名前をClockTimerとします。
main.dart
class ClockTimer extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ClockTimerState();
  }
}
class _ClockTimerState extends State<ClockTimer> {
  /// 初期値
  var _timeString = '00:00:00';
  var _isStart = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('タイマーアプリ')
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Text(_timeString, style: TextStyle(fontSize: 60)),
          ),
          Container(
            width: 100,
            height: 50,
            color: Colors.greenAccent,
            child: TextButton(
                onPressed: null,
                child: Text(_isStart ? 'STOP' : 'START')),
          )
        ],
      ),
    );
  }
}これでアプリをビルドすると次のような画面が表示されると成功です。

まだSTARTボタンをタップしても何も起きません。
このボタンタップ時の処理をこれから作っていきます。
1. スタートボタンをタップしたらその時の現在時刻を取得して保持する
まずはスタートボタンタップ時に走る関数を_startTimer()と命名します。_startTimer()の中身は現時点では何もしません。
main.dart
Container(
            width: 100,
            height: 50,
            color: Colors.greenAccent,
            child: TextButton(
                onPressed: _startTimer,
                child: Text(_isStart ? 'STOP' : 'START')
            ),
          )
  void _startTimer() {
  }それではこの_startTimer関数の処理を書いていきます。
/// 開始時間
  DateTime _startTime;
  void _startTimer() {
    setState(() {
        _startTime = DateTime.now();
    });
  }ClockTimerクラスにプロパティ変数として_startTimeと_timerを追加しました。着目すべきところは、
_startTime = DateTime.now(); で現在時刻を取得してタイマーを開始しています。
2. ポーリング処理で1秒ごとにその時の時刻を取得し、1で取得した時刻との差分を取る
ボタンをタップして現在時刻を取得できましたので、次にタイマー処理の部分を作っていきます。これも前のストップウォッチの時に実装したことと同じような事です。
Timerクラスを使うためには、dart:asyncのDartライブラリが必要なのでインポートする必要があります。
main.dartのクラスファイルの一番上に次の1行を書いてみましょう。
import 'dart:async';これでTimerクラスが使えるようになります。それでは改めてポーリング処理の実装を書いていきましょう。
main.dart
/// ローカルタイマー
  var _timer;
  void _startTimer() {
    setState(() {
        _startTime = DateTime.now();
        _timer  = Timer.periodic(Duration(seconds: 1), _onTimer);
    });
  }
  void _onTimer(Timer timer) {
    /// 現在時刻を取得
    var now = DateTime.now();
    /// 開始時刻と比較して差分を取得
    var diff = now.difference(_startTime).inSeconds;
  }これで、1秒おきに何かしらの処理をするポーリング処理を実装できました。
新しく_onTimer()メソッドの中で1秒ごとの現在時刻を再度取得しています。そして、DateTimeクラスにはDateTimeのインスタンス同士の差分を取得できるdifference(日付)メソッドがあります。このdifferenceメソッドでAとB間の日付の差分を取得します。差分の型はDurationです。
external Duration difference(DateTime other);| Properties | return (返り値) | 意味 | 
| inDays | int | 日 | 
| inHours | int | 時 | 
| inMinutes | int | 分 | 
| inSeconds | int | 秒 | 
今回必要なのは、秒単位なので「inSeconds」を使いました。
3. タイマー表示部分の計算ロジックを書く
それでは、やっとタイマー部分の表示ロジックの計算に入ります。
ここでは_onTimerメソッドを深堀りします。
void _onTimer(Timer timer) {
    /// 現在時刻を取得
    var now = DateTime.now();
    /// 開始時刻と比較して差分を取得
    var diff = now.difference(_startTime).inSeconds;
    /// タイマーのロジック
    var hour = (diff / (60 * 60)).floor();
    var mod = diff % (60 * 60);
    var minutes = (mod / 60).floor();
    var second = mod % 60;
  }こんな感じで計算します。「タイマーのロジック」の部分です。これでそれぞれ、「時」「分」「秒」の数字を計算します。
4. タイマーらしく00:00:00 (時:分:秒)表記の文字列に変換する
ほとんど最後の作業ですが、タイマー表記にしたいので、得られた各数値を文字列に変換します。
main.dart
String _convertTwoDigits(int number) {
    return number >= 10 ? "$number" : "0$number";
  }_convertTwoDigits関数は、intからStringに変換するメソッドです。これを使って先程得られた数値を文字列に変換します。
main.dart
void _onTimer(Timer timer) {
    /// 現在時刻を取得
    var now = DateTime.now();
    /// 開始時刻と比較して差分を取得
    var diff = now.difference(_startTime).inSeconds;
    /// タイマーのロジック
    var hour = (diff / (60 * 60)).floor();
    var mod = diff % (60 * 60);
    var minutes = (mod / 60).floor();
    var second = mod % 60;
    setState(() => {
      _timeString = """${_convertTwoDigits(hour)}:${_convertTwoDigits(minutes)}:${_convertTwoDigits(second)}"""
    });
  }
  String _convertTwoDigits(int number) {
    return number >= 10 ? "$number" : "0$number";
  }_onTimerメソッドを上記のように修正しました。これはちょっと汚い感がありますが気にしないでおきましょう。これで_timeStringに「00:00:00」表記の文字列が入りました。
5. 4で計算した文字列を画面に表示させる
殆ど終わっているようなものですが、前項で_timeStringに開始日付からの差分がタイマー表記が入っていますので、これをTextウィジェットに入れたらOKです。
あとは、タイマーアプリらしく、「開始中」と「停止中」の切り替えができるように_isStartというbool型のプロパティを用意して、タイマーのon/offを切り替えられるようにします。
main.dart
var _isStart = false;
  void _startTimer() {
    setState(() {
      _isStart = !_isStart;
      if (_isStart) {
        _startTime = DateTime.now();
        _timer  = Timer.periodic(Duration(seconds: 1), _onTimer);
      } else {
        _timer.cancel();
      }
    });
  }こんなふうに_startTimerメソッドを修正してタイマー作動中にボタンが押されたらタイマーを停止できるようしました。
if (_isStart) {
        _startTime = DateTime.now();
        _timer  = Timer.periodic(Duration(seconds: 1), _onTimer);
      } else {
        _timer.cancel();
      }これがその条件分岐です。
そして、アプリのレイアウトの部分のロジックは次のように変わります。
main.dart
@override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('タイマーアプリ')
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Text(_timeString, style: TextStyle(fontSize: 60)),
          ),
          Container(
            width: 100,
            height: 50,
            color: Colors.greenAccent,
            child: TextButton(
                onPressed: _startTimer,
                child: Text(_isStart ? 'STOP' : 'START')),
          )
        ],
      ),
    );
  }これでタイマーアプリが出来上がりました。
ややこしいと思いますので、ClockTimerクラスの全体像をもう一度貼り付けます。
main.dart
class ClockTimer extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ClockTimerState();
  }
}
class _ClockTimerState extends State<ClockTimer> {
  /// 初期値
  var _timeString = '00:00:00';
  /// 開始時間
  DateTime _startTime;
  /// ローカルタイマー
  var _timer;
  var _isStart = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('タイマーアプリ')
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Text(_timeString, style: TextStyle(fontSize: 60)),
          ),
          Container(
            width: 100,
            height: 50,
            color: Colors.greenAccent,
            child: TextButton(
                onPressed: _startTimer,
                child: Text(_isStart ? 'STOP' : 'START')),
          )
        ],
      ),
    );
  }
  void _startTimer() {
    setState(() {
      _isStart = !_isStart;
      if (_isStart) {
        _startTime = DateTime.now();
        _timer  = Timer.periodic(Duration(seconds: 1), _onTimer);
      } else {
        _timer.cancel();
      }
    });
  }
  void _onTimer(Timer timer) {
    /// 現在時刻を取得
    var now = DateTime.now();
    /// 開始時刻と比較して差分を取得
    var diff = now.difference(_startTime).inSeconds;
    /// タイマーのロジック
    var hour = (diff / (60 * 60)).floor();
    var mod = diff % (60 * 60);
    var minutes = (mod / 60).floor();
    var second = mod % 60;
    setState(() => {
      _timeString = """${_convertTwoDigits(hour)}:${_convertTwoDigits(minutes)}:${_convertTwoDigits(second)}"""
    });
  }
  String _convertTwoDigits(int number) {
    return number >= 10 ? "$number" : "0$number";
  }
}このように修正してアプリをビルドすると、次のような画面が表示されます。STARTボタンをタップするとタイマーが開始して「STOP」ボタンに切り替わります。「STOP」ボタンをタップするとタイマーが停止します。

これでやっとタイマーアプリが完成しました。このロジックを使ってオリジナルアプリに流用したいのですが、その辺りの内容はまた別の機会に紹介しようかなと思います。
今日はここまでです。
それでは、バイバイ。
アプリ全体のソースコード
最後の補足として、今回のタイマーアプリの全体のソースコードを貼り付けます。
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Timer App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ClockTimer(),
    );
  }
}
class ClockTimer extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _ClockTimerState();
  }
}
class _ClockTimerState extends State<ClockTimer> {
  /// 初期値
  var _timeString = '00:00:00';
  /// 開始時間
  DateTime _startTime;
  /// ローカルタイマー
  var _timer;
  var _isStart = false;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('タイマーアプリ')
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Center(
            child: Text(_timeString, style: TextStyle(fontSize: 60)),
          ),
          Container(
            width: 100,
            height: 50,
            color: Colors.greenAccent,
            child: TextButton(
                onPressed: _startTimer,
                child: Text(_isStart ? 'STOP' : 'START')),
          )
        ],
      ),
    );
  }
  void _startTimer() {
    setState(() {
      _isStart = !_isStart;
      if (_isStart) {
        _startTime = DateTime.now();
        _timer  = Timer.periodic(Duration(seconds: 1), _onTimer);
      } else {
        _timer.cancel();
      }
    });
  }
  void _onTimer(Timer timer) {
    /// 現在時刻を取得
    var now = DateTime.now();
    /// 開始時刻と比較して差分を取得
    var diff = now.difference(_startTime).inSeconds;
    /// タイマーのロジック
    var hour = (diff / (60 * 60)).floor();
    var mod = diff % (60 * 60);
    var minutes = (mod / 60).floor();
    var second = mod % 60;
    setState(() => {
      _timeString = """${_convertTwoDigits(hour)}:${_convertTwoDigits(minutes)}:${_convertTwoDigits(second)}"""
    });
  }
  String _convertTwoDigits(int number) {
    return number >= 10 ? "$number" : "0$number";
  }
}





