くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Riverpodでstop/restartができるカウントダウンタイマーを作る

カウントダウンタイマーがほしくて、いろいろみていたけど、
よさそうなのがなかったので、試してみたときの備忘録(*´ω`*)

ほしいもの

  • start/stop/restart/resetができる
  • 初期値/インターバルが設定できる
  • 現在の時間が取得/listenできる

Timer.periodicStopwatchもあるけど、
停止ができなかったり、途中の時間が取れなかったりしたので自作した。

できたもの

// count_down_timer.dart
import 'dart:async';

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'count_down_timer.freezed.dart';
part 'count_down_timer.g.dart';

@freezed
class CountDownTimerState with _$CountDownTimerState {
  const CountDownTimerState._();
  const factory CountDownTimerState({
    // カウントダウン中かどうかのフラグ
    required bool isRunning,
    // 残り時間の初期値
    required Duration initial,
    // インターバルの間隔
    required Duration interval,
    // 現在の残り時間
    required Duration current,
    
    // 内部で使うTimer
    required Timer? timer,
  }) = _CountDownTimerState;
}

@riverpod
class CountDownTimer extends _$CountDownTimer {
  @override
  CountDownTimerState build({
    required Duration initial,
    required Duration interval,
  }) {
    ref.onDispose(() {
      state.timer?.cancel();
    });
    return CountDownTimerState(
      isRunning: false,
      interval: interval,
      initial: initial,
      current: initial,
      timer: null,
    );
  }

  // ********************************************************
  // * actions
  // ********************************************************
  // カウントダウンの開始
  void start() {
    if (state.isRunning) return;
    state = state.copyWith(isRunning: true, timer: _createTimer());
  }

  // カウントダウンの停止
  void stop() {
    if (!state.isRunning) return;
    // Timerを破棄する
    state.timer?.cancel();
    state = state.copyWith(isRunning: false, timer: null);
  }

  // 残り時間のリセット: 初期値に戻す
  void reset() {
    state = state.copyWith(current: initial);
  }

  // カウントダウンのリスタート
  void restart() {
    state.timer?.cancel();
    state = state.copyWith(
      current: initial,
      isRunning: true,
      timer: _createTimer(),
    );
  }

  // ********************************************************
  // * private
  // ********************************************************
  // インターバルごとに呼び出されるTimerを作成
  Timer _createTimer() {
    return Timer.periodic(state.interval, _onTick);
  }

  // インターバルごとに呼び出されたときの処理
  void _onTick(Timer timer) {
    // 停止中ならなにもしない
    if (!state.isRunning) return;
    
    // 残り時間を計算
    final current = state.current - state.interval;
    
    // 残り時間が0以下かどうかの判定
    final isOver = current.compareTo(Duration.zero) < 0;
    if (isOver) {
      // 残り時間が0ならTimerを停止
      state.timer?.cancel();
      state = state.copyWith(current: Duration.zero, timer: null);
    } else {
      // 残り時間が0より大きい場合は、残り時間を更新
      state = state.copyWith(current: current);
    }
  }
}

使い方

使い方はこんな感じ

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

class CountDownTimerScreen extends HookConsumerWidget {
  const CountDownTimerScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // カウントダウンタイマーの準備
    final myTimer = countDownTimerProvider(
      initial: const Duration(seconds: 3),
      interval: const Duration(milliseconds: 10),
    );
    // 現在の時刻をwatch
    final currentTime = ref.watch(myTimer.select((value) => value.current));

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text('time: ${currentTime}'),
        ElevatedButton(
          onPressed: () {
            ref.read(myTimer.notifier).start();
          },
          child: const Text("Start"),
        ),
        ElevatedButton(
          onPressed: () {
            ref.read(myTimer.notifier).stop();
          },
          child: const Text("Stop"),
        )
      ],
    );
  }
}

以上!! 地味に便利。。(*´ω`*)