flutter_riverpodを使ってタイマー(Timer)を作成

Flutter
 

Flutterを使ってタイマー(Timer)を作成してみます。

ただStatefulWidgetを使ってタイマー(Timer)を作成することもできますが、単一責任の原則を意識するためにflutter_riverpodを使ってタイマー(Timer)を作成してみます。

実装方法

以下の3Stepで実装を進めていきます

  • Timerクラスで作成した時間を表示する
  • flutter_riverpodでロジック分離する
  • スタート・リセットボタンを追加

Timerクラスで作成した時間を表示する

事前にflutter_riverpodをインストールしておいてください。

flutter pub add flutter_riverpod

まずはタイマークラス(Timer)にて定義された変数を画面に表示してみます。

import 'dart:async';

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

void main() {
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  Timer? timer;
  Duration timerDuration = Duration(minutes: 1);
  String strDigits(int n) => n.toString().padLeft(2, '0');

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
        ),
        useMaterial3: true,
      ),
      home: Scaffold(
        body: SafeArea(
          child: Text(
            strDigits(timerDuration.inMinutes.remainder(60)) +
                ':' +
                strDigits(timerDuration.inSeconds.remainder(60)),
            style: TextStyle(
              fontSize: 56,
            ),
          ),
        ),
      ),
    );
  }
}

flutter_riverpodでロジック分離する

上記でタイマーの表示はできました。

ただ、ロジックと画面描画が同じファイルにあることが処理を分かりづらくします。

そこで、ロジック部分の処理をmodelに画面描画をviewに分けることを行います。

main.dart

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

import 'home/view.dart';

void main() {
  runApp(
    ProviderScope(
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
        ),
        useMaterial3: true,
      ),
      home: Home(),
    );
  }
}

model.dart

import 'dart:async';

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

final timerProvider = ChangeNotifierProvider((ref) => TimerModel());

class TimerModel extends ChangeNotifier {
  Timer? timer;
  Duration timerDuration = Duration(minutes: 1);
  String minutes = "00";
  String seconds = "00";

  TimerModel() {
    featch();
  }

  void refresh() {
    String strDigits(int n) => n.toString().padLeft(2, '0');
    minutes = strDigits(timerDuration.inMinutes.remainder(60));
    seconds = strDigits(timerDuration.inSeconds.remainder(60));
    notifyListeners();
  }

view.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tmp_blog/home/model.dart';

class Home extends ConsumerWidget {
  const Home({
    super.key,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final TimerModel timerModel = ref.watch(timerProvider);
    return Scaffold(
      body: SafeArea(
        child: Text(
          timerModel.minutes + ':' + timerModel.seconds,
          style: TextStyle(
            fontSize: 56,
          ),
        ),
      ),
    );
  }
}

スタート・リセットボタンを追加

タイマーをスタート・ストップするボタンを追加します。

タイマーを始めるときはボタンの表示を「Start」にし、
タイマーが進んだときは「Stop」を表示するように制御します。

上記の制御はタイマーが1分の時に「Start」、1分ではない時に「Stop」にするようにしていますが、
スタートの直前で連続して押下するとその判定がバグってしまいます。

そのために連続して押下できないような制御をフラグを使って実装しています。

model.py

import 'dart:async';

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

final timerProvider = ChangeNotifierProvider((ref) => TimerModel());

class TimerModel extends ChangeNotifier {
  Timer? timer;
  Duration timerDuration = Duration(minutes: 1);
  String minutes = "00";
  String seconds = "00";

  bool isDisabledButton = false;
  String buttonDisplay = "start";
  Color foregroundColor = Color.fromARGB(255, 255, 199, 0);
  Color backgroundColor = Color.fromARGB(255, 253, 251, 240);

  TimerModel() {
    _refresh();
  }

  void _refresh() {
    String strDigits(int n) => n.toString().padLeft(2, '0');
    minutes = strDigits(timerDuration.inMinutes.remainder(60));
    seconds = strDigits(timerDuration.inSeconds.remainder(60));
    notifyListeners();
  }

  void setCountDown() {
    final reduceSecondsBy = 1;
    final seconds = timerDuration.inSeconds - reduceSecondsBy;
    if (seconds < 0) {
      timer!.cancel();
    } else {
      timerDuration = Duration(seconds: seconds);
    }
  }

  void start() {
    timer = Timer.periodic(Duration(seconds: 1), (_) {
      setCountDown();
      isDisabledButton = false;
      _refresh();
    });
  }

  void reset() {
    timer!.cancel();
    timerDuration = Duration(minutes: 1);
    isDisabledButton = false;
    _refresh();
  }

  void doStartOrReset() {
    // 連続して押下することを回避
    if (isDisabledButton) {
      return;
    }
    isDisabledButton = true;
    if (timerDuration == Duration(minutes: 1)) {
      start();
      buttonDisplay = "Reset";
      foregroundColor = Color.fromARGB(255, 113, 113, 113);
      backgroundColor = Color.fromARGB(255, 253, 251, 240);
    } else {
      reset();
      buttonDisplay = "Start";
      foregroundColor = Color.fromARGB(255, 255, 199, 0);
      backgroundColor = Color.fromARGB(255, 253, 251, 240);
    }
    notifyListeners();
  }
}

view.py

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tmp_blog/home/model.dart';

class Home extends ConsumerWidget {
  const Home({
    super.key,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final TimerModel timerModel = ref.watch(timerProvider);
    return Scaffold(
      body: SafeArea(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(
                timerModel.minutes + ':' + timerModel.seconds,
                style: TextStyle(
                  fontSize: 56,
                ),
              ),
              ElevatedButton(
                onPressed: () {
                  timerModel.doStartOrReset();
                },
                child: Text(timerModel.buttonDisplay),
                style: ElevatedButton.styleFrom(
                  foregroundColor: timerModel.foregroundColor,
                  backgroundColor: timerModel.backgroundColor,
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

コメント

タイトルとURLをコピーしました