Search code examples
flutterriverpodflutter-state

Why is my text not updating in my widget?


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?


Solution

  • 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"))
        ]);
      }
    }