1

I'm trying to make a simple timer using Riverpod and changeNotifier

So I followed the todo example of the doc and came up with that :

class ClickTimer {
  ClickTimer({required this.startTime});
  int startTime;
}

class TimerNotifier extends ChangeNotifier {
  final countDownTimer = <ClickTimer>[];

  int _seconds = 20;
  Timer? _timer;

  void _startTimer() {
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      if (_seconds > 0) {
        _seconds--;
      } else {
        _timer!.cancel();
      }
      notifyListeners();
    });
  }

  void increaseTimer() {
    _seconds += 3;
    notifyListeners();
  }

  void reduceTimer() {
    _seconds -= 3;
    notifyListeners();
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }
}

final timerProvider = ChangeNotifierProvider<TimerNotifier>((ref) {
  return TimerNotifier();
});

And for the UI I used a consumer widget like that :

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(children: [
      Text("${ref.read(timerProvider.notifier)._seconds}"),
      ElevatedButton(
          onPressed: () {
            ref.read(timerProvider.notifier)._startTimer();
            print(ref.read(timerProvider.notifier)._seconds);
          },
          child: Text("Start timer"))
    ]);
  }
}

When I click on my button it print the value of seconds and every time I click it decrease but the text displayed doesn't change at all (only in my console and only when I click) Why is that? How to fix this issue?

Coca95
  • 31
  • 5

1 Answers1

1

The short answer: You have to use ref.watch instead of ref.read inside your widgets if you want an automatic rerender. With ref.read the expression is only executed once and not with every change.

But: Riverpod recommendeds to use a StateNotifier, instead of a ChangeNotifier. With a StateNotifier, you can keep your current seconds in the state and the Text gets updated with ref.watch() each time the state changes without the need to call notifyListeners() each time. This way the functionality is more reliable and comprehensible than with the ChangeNotifier.

To do so, your code needs to be refactored like this:

class TimerNotifier extends StateNotifier<int> {

  // initialize seconds
  TimerNotifier() : super(20);

  Timer? _timer;

  void startTimer() {
    _timer = Timer.periodic(Duration(seconds: 1), (timer) {
      if (state > 0) {
        state -= 1;
      } else {
        _timer!.cancel();
      }
    });
  }

  void increaseTimer() {
    state += 3;
  }

  void reduceTimer() {
    state -= 3;
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }
}

final timerProvider = StateNotifierProvider<TimerNotifier, int>(
  (ref) => TimerNotifier(),
);

And for the UI like that :

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(children: [
      Text("${ref.watch(timerProvider)}"),
      ElevatedButton(
          onPressed: () {
            ref.read(timerProvider.notifier).startTimer();
            print(ref.read(timerProvider));
          },
          child: Text("Start timer"))
    ]);
  }
}
Michel
  • 206
  • 5
  • Thank you so much! I finally get the difference between read and watch. Wasn't very clear for me! And so if I want to execute a method from my TimerNotifier I user ref.read but if I only want to display a valu it'll be ref.watch? – Coca95 Apr 05 '23 at 14:25
  • Basically yes. With the addition, that inside of methods and functions, you can always use ref.read, because there is nothing that can be rerendered. Maybe you can think of it this way: if you want display something that changes when the value changes, you need ref.watch and if you want to call a function or read the value only once, you need ref.read. – Michel Apr 05 '23 at 14:28
  • Ok thanks a lot for clarifying that! Quick question, if I want to navigate to another page when the timer ends I cant use Navigator.push in the StateNotifier. Should I require this function for when I declare the timer in my widget tree and the declare the function there? And then I should leave it empty in my stateNotifier? – Coca95 Apr 05 '23 at 14:38
  • Check out the ref.listen function. Put it inside your build function above the return (you can also put it inside the build function of the MainApp Widget, to be independent from the active page in your app) and then you can listen for changes in the StateNotifier, check the new state and previous state and react accordingly (for example changing the Navigation). – Michel Apr 05 '23 at 14:44