実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

Flutterアプリで定期実行処理・Timer

はじめに

今回は定期的に処理を実行するためのタイマーと、おまけで後半は見栄えの良いタイマーをお手軽に作れるパッケージを紹介していきます。

重要なのは「定期的に処理を実行できる」仕組みということで、使いみちは様々あります。ゲームやアニメーション処理で定期的にウィジェットをリライトするとか、定期的にウェブアクセスするとか、色々ですね。ユーザの操作きっかけではなく、自発的にタイミングを見て動くアプリを実装する場合には、かなりこの定期的な処理実行が出てくると思います。

その使い方を簡単に説明するために定番のストップウォッチでサンプルを示しますが、こういうタイマーを作るためだけのものではありませんので念の為。

後半はタイマー繋がりで、お手軽にそこそこの見栄えのするタイマー表示ウィジェットを紹介します。使い方は非常に簡単で、定番の丸い円のタイマーを作成できますので便利です。






定期割り込み処理の方法

サンプルコードの動作をスクリーンショットでまずは以下に示していきます。

スクリーンショット

最初に数字のゼロとボタンが表示されます。ボタンを押すとカウントアップがスタートします:

f:id:linkedsort:20211128183324p:plain:w350

カウントアップがスタートするとボタンは「Pause」の表記になります。これを押すと、一時停止になります:

f:id:linkedsort:20211128183400p:plain:w350

「Pause」ボタンを押すと一時停止して、ボタン表記は「Run」に戻ります。もう一度押すと、カウントアップが再開されます:

f:id:linkedsort:20211128183422p:plain:w350


サンプルコード

以下にサンプルコードを示していきます。GetXによる状態管理を利用していますので、導入の仕方がもしわからなければ下記をご参照下さい:


flutter.gakumon.jp

ただ定期実行のポイントを掴む意味では特にGetXは関係ありませんので、サンプルコードのポイントの部分を見ていくだけでもOKです:

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

void main() {
  runApp(const MyApp());
}

//(1) アプリの状態管理
class Controller extends GetxController {
  //カウント
  var count = 0.obs;

  //タイマーが進行中はtrue
  var isRunning = false.obs;
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const GetMaterialApp(
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var cntl = Get.put(Controller());
    //(2) タイマーの基本設定
    var timer = Timer.periodic(const Duration(seconds: 1), (_) {
      if (cntl.isRunning.value) {
        cntl.count.value++;
      }
    });

    return Scaffold(
        appBar: AppBar(
          title: const Text("ホーム"),
        ),
        body: Center(
            child: Column(children: [
          const Padding(padding: EdgeInsets.all(30)),
          //(3) タイマー表示
          Obx(() => Text(
                "${cntl.count.value}",
                style: const TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: 32,
                ),
              )),
          const Padding(padding: EdgeInsets.all(10)),
          //(4) ボタン操作
          Obx(() => ElevatedButton(
              onPressed: () {
                cntl.isRunning.value = !cntl.isRunning.value;
              },
              child: Text(cntl.isRunning.value ? "Pause" : "Run")))
        ])));
  }
}

コードのポイント

サンプルコードのうち番号を付したコメントの部分についてポイントを解説していきます。

(1) 状態管理用コントローラ
//(1) アプリの状態管理
class Controller extends GetxController {
  //カウント
  var count = 0.obs;

  //タイマーが進行中はtrue
  var isRunning = false.obs;
}

お馴染みのGetxControllerに今回はカウントアップしていくカウンターと、タイマーが動作している/一時停止中の状態をもたせます。

お馴染みじゃないよという方は下記を参照ください:


flutter.gakumon.jp

でもGetX状態管理のことはとりあえずおいておいて、定期実行の部分だけを見たいという方は気にせず次に進まれてOKです。

(2) 定期実行のための仕掛け
  //(2) タイマーの基本設定
  var timer = Timer.periodic(const Duration(seconds: 1), (_) {
    if (cntl.isRunning.value) {
      cntl.count.value++;
    }
  });

Timerクラスを使っていきます。このため冒頭で「import 'dart:async';」が必要になります。

Timer.periodicにDurationで割り込みの間隔とその間隔毎に呼び出すコールバック関数を指定しています。ここでは1秒毎に、次に続く関数を呼び出しています。secondsのところをmillisecondsにすれば指定のミリ秒毎に割り込みをかけることができます。

(3) カウントの数字を表示
  //(3) タイマー表示
  Obx(() => Text(
        "${cntl.count.value}",
        style: const TextStyle(
        fontWeight: FontWeight.bold,
        fontSize: 32,
      ),
    )),

カウントの数字が変わるたびに数次の表示を変更しています。GetXの仕組みに従ってObx( ()=> )でTextウィジェットを囲むだけでOKです。

(4) 実行/一時停止トグルボタン
  //(4) ボタン操作
  Obx(() => ElevatedButton(
      onPressed: () {
        cntl.isRunning.value = !cntl.isRunning.value;
      },
      child: Text(cntl.isRunning.value ? "Pause" : "Run")))

ボタンが押されるたびに、実行状態と一時停止状態(isRunningの真偽)を切り替えています。そして状態に従ってラベルの文字列を切り替えるため、こちらもObxで囲みます。状態が変わるたびにリライトする必要がありますからね。

おしゃれタイマー

冒頭にも書きましたが、本記事の主題は「定期実行の仕組み」の解説ということで「タイマー」の実装というわけではないのですが、ついでですのでタイマーをお手軽実装する人気のパッケージを紹介したいと思います:


pub.dev

こちらと、状態管理としてGetXを使ってきます。

スクリーンショット

サンプルのスクリーンショットです。見た目はこのように、数次をぐるっと囲った丸という構成です:

f:id:linkedsort:20211129214950p:plain:w350

「開始」ボタンを押すと、円グラフに進捗が表示されていくスタイルです:

f:id:linkedsort:20211129215026p:plain:w350

サンプルコード

以下にサンプルコードを示します。依存パッケージとして「circular_countdown_timer」と「get」を使いますので、Terminalを起動して下記を実行するか、pubspec.yamlを直接編集して下さい:

flutter pub add circular_countdown_timer
flutter pub add get
flutter pub get

サンプルコードは以下です:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:circular_countdown_timer/circular_countdown_timer.dart';

void main() {
  runApp(const MyApp());
}

//(1) コントローラー
class Controller extends GetxController {
  var cdCntl = CountDownController().obs;
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const GetMaterialApp(
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var cntl = Get.put(Controller());
    int initCount = 30;

    return Scaffold(
        appBar: AppBar(
          title: const Text("ホーム"),
        ),
        body: Center(
            child: Column(children: [
          const Padding(padding: EdgeInsets.all(30)),
          //(2) カウントダウンタイマー
          Obx(() => CircularCountDownTimer(
                duration: initCount,
                initialDuration: 0,
                controller: cntl.cdCntl.value,
                width: MediaQuery.of(context).size.width / 3,
                height: MediaQuery.of(context).size.height / 3,
                ringColor: Colors.lightBlueAccent,
                ringGradient: null,
                fillColor: Colors.blueAccent,
                fillGradient: null,
                backgroundColor: Colors.white,
                backgroundGradient: null,
                strokeWidth: 20,
                strokeCap: StrokeCap.round,
                textStyle: const TextStyle(
                    fontSize: 64,
                    color: Colors.blue,
                    fontWeight: FontWeight.bold),
                isReverse: false,
                isReverseAnimation: false,
                isTimerTextShown: true,
                autoStart: false,
              )),
          const Padding(padding: EdgeInsets.all(10)),
          //(3) 各種ボタン
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                  onPressed: () => cntl.cdCntl.value.start(),
                  child: const Text("開始")),
              ElevatedButton(
                  onPressed: () => cntl.cdCntl.value.pause(),
                  child: const Text("一時停止")),
              ElevatedButton(
                  onPressed: () => cntl.cdCntl.value.resume(),
                  child: const Text("再開")),
              ElevatedButton(
                  onPressed: () =>
                      cntl.cdCntl.value.restart(duration: initCount),
                  child: const Text("再スタート")),
            ],
          ),
        ])));
  }
}

コードのポイント

下記にポイントを挙げていきます。

(1) GetXコントローラー
//(1) コントローラー
class Controller extends GetxController {
  var cdCntl = CountDownController().obs;
}

このタイマーは「CountDownController」のインスタンスをコントローラとして使いますので、これにobsをくっつけてGetXコントローラーとします。

(2) タイマーの設定
  //(2) カウントダウンタイマー
  Obx(() => CircularCountDownTimer(
      duration: initCount,
      initialDuration: 0,
      controller: cntl.cdCntl.value,
      width: MediaQuery.of(context).size.width / 3,
      height: MediaQuery.of(context).size.height / 3,
      ringColor: Colors.lightBlueAccent,
      ringGradient: null,
      fillColor: Colors.blueAccent,
      fillGradient: null,
      backgroundColor: Colors.white,
      backgroundGradient: null,
      strokeWidth: 20,
      strokeCap: StrokeCap.round,
      textStyle: const TextStyle(
      fontSize: 64,
      color: Colors.blue,
      fontWeight: FontWeight.bold),
      isReverse: false,
      isReverseAnimation: false,
      isTimerTextShown: true,
      autoStart: false,
  )),

タイマーのコンストラクタです。各種設定値を指定していきます。

controllerに前出のコントローラを指定して紐付けておきます。durationはカウントのゴールになる値、initalDurationが開始時の値です。

あとはサイズはカラーですので大体わかると思います。色々と指定して試してみて下さい。Gradientはグラデーションの設定です。グラデーションについては下記ページのグラデーションの節をご参照下さい:


flutter.gakumon.jp

(3) 各種操作
  //(3) 各種ボタン
 Row(
   mainAxisAlignment: MainAxisAlignment.center,
   children: [
     ElevatedButton(
       onPressed: () => cntl.cdCntl.value.start(),
       child: const Text("開始")),
     ElevatedButton(
       onPressed: () => cntl.cdCntl.value.pause(),
       child: const Text("一時停止")),
     ElevatedButton(
       onPressed: () => cntl.cdCntl.value.resume(),
       child: const Text("再開")),
     ElevatedButton(
       onPressed: () => cntl.cdCntl.value.restart(duration: initCount),
       child: const Text("再スタート")),
    ],
  ),

タイマーの操作はコントローラにstart()、pause()、resume()、restart()を入力していきます。意味はそれぞれボタンのラベルにある通りです。

GetXコントローラで管理していますので「value」が挟まることに注意です。






おわりに

今回はTimerによる定期実行について取り上げました。

自分はゲームが好きなのですが、画面の定期再描画や一定時間立ったときにイベントを発生させるなどにお手軽に活用できます。

Timer自体は非常にシンプルですが、定期的にコールバックしてくれさえすれば、あとは自分でカウントするなり状態フラグを管理することで様々な処理を実現することができますので、色々工夫してみてください。

f:id:linkedsort:20211129231923j:plain