5

I have built a working countdown clock that displays the time remaining in the format, hours:minutes:senconds. inside a stateful widget. that uses a fullscreen inkwell to start and stop it. What I want to do is transfer this code to a change notifier. (I already a change notifier setup ready to go called CountdownTimers) so i can see this countdown running from multiple pages on my app.

Here is the code for the working countdown clock in the stateful widget:

class ChessClock2 extends StatefulWidget {
  const ChessClock2({Key? key}) : super(key: key);

  @override
  State<ChessClock2> createState() => _ChessClock2State();
}

class _ChessClock2State extends State<ChessClock2> {
  static const countdownDuration = Duration(hours: 5, minutes: 10, seconds: 10);
  Duration duration = Duration();
  Timer? timer;

  bool beenPressed = false;

  @override
  void initState() {
    super.initState();
    Reset();
  }

  void Reset() {
    setState(() => duration = countdownDuration);
  }

  void AddTime() {
    final minusSeconds = -1;
    setState(() {
      final seconds = duration.inSeconds + minusSeconds;
      if (seconds < 0) {
        timer?.cancel();
      } else {
        duration = Duration(seconds: seconds);
      }
    });
  }

  void StartTimer() {
    timer = Timer.periodic(
      Duration(seconds: 1),
      (_) => AddTime(),
    );
  }

  void StopTimer() {
    setState(() => timer?.cancel());
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: InkWell(
          onTap: () {
            setState(() {
              beenPressed = !beenPressed;
            });
            beenPressed ? StartTimer() : StopTimer();
          },
          child: Container(
            width: double.infinity,
            height: double.infinity,
            decoration: BoxDecoration(
              color: beenPressed ? kOrange : kBlueGrey900,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Center(
              child: TimeDisplay(),
            ),
          ),
        ),
      ),
    );
  }

  Widget TimeDisplay() {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final hours = twoDigits(
      duration.inHours.remainder(60),
    );
    final minutes = twoDigits(
      duration.inMinutes.remainder(60),
    );
    final seconds = twoDigits(
      duration.inSeconds.remainder(60),
    );
    return Text(
      '$hours:$minutes:$seconds',
      style: TextStyle(fontSize: 50),
    );
  }
}

When I transfer the code over, I'm running into trouble because I can't use setState in the change notifier and I'm unsure how to translate the code to get it to work. so far, by moving the individual variables over as well as the widget TimDisplay, I'm able to get the timer to display correctly from the change notifier but am not sure how to get it to work from the change notifier. here is where I am now:

type hereclass ChessClock3 extends StatefulWidget {
  const ChessClock3({Key? key}) : super(key: key);

  @override
  State<ChessClock3> createState() => _ChessClock3State();
}

class _ChessClock3State extends State<ChessClock3> {
  @override
  void initState() {
    super.initState();
    Reset();
  }

  void Reset() {
    setState(() => duration = countdownDuration);
  }

  void AddTime() {
    final minusSeconds = -1;

    setState(() {
      final seconds = duration.inSeconds + minusSeconds;
      if (seconds < 0) {
        timer?.cancel();
      } else {
        duration = Duration(seconds: seconds);
      }
    });
  }

  void StartTimer() {
    timer = Timer.periodic(
      Duration(seconds: 1),
      (_) => AddTime(),
    );
  }

  void StopTimer() {
    setState(() => timer?.cancel());
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: InkWell(
          onTap: () {
            setState(() {
              context.read<CountdownTimers>().BeenPressed();
            });
            context.read<CountdownTimers>().beenPressed
                ? StartTimer()
                : StopTimer();
          },
          child: Container(
            width: double.infinity,
            height: double.infinity,
            decoration: BoxDecoration(
              color: context.watch<CountdownTimers>().beenPressed
                  ? kKillTeamOrange
                  : kBlueGrey900,
              borderRadius: BorderRadius.circular(30),
            ),
            child: Center(
              child: context.read<CountdownTimers>().TimeDisplay(),
            ),
          ),
        ),
      ),
    );
  }
}

class CountdownTimers with ChangeNotifier {
  Duration _countdownDuration = Duration(hours: 5, minutes: 10, seconds: 10);
  Duration _duration = Duration();
  Timer? timer;
  bool _beenPressed = false;

  Duration get countdownDuration => _countdownDuration;
  Duration get duration => _duration;
  bool get beenPressed => _beenPressed;

  void BeenPressed() {
    _beenPressed = !_beenPressed;
  }

  Widget TimeDisplay() {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final hours = twoDigits(
      _duration.inHours.remainder(60),
    );
    final minutes = twoDigits(
      _duration.inMinutes.remainder(60),
    );
    final seconds = twoDigits(
      _duration.inSeconds.remainder(60),
    );
    return Text(
      '$hours:$minutes:$seconds',
      style: TextStyle(fontSize: 50),
    );
  }
}

If anyone can show me how to translate the code over It would be very much appreciated. thanks so much!

jraufeisen
  • 3,005
  • 7
  • 27
  • 43
Nomad09
  • 161
  • 8
  • I would suggest trimming down the example in your code snippets to the minimal parts. No need for the specifics of a BoxDecoration for example. This makes the question easier to for others – jraufeisen Jan 23 '23 at 22:49

2 Answers2

0

I would use Flutter Riverpod instead like the code below. In Riverpod it's not recommended to use Change Notifier. Even in complexe application. To change the state of Change Notifier you must call notifyListeners.

import 'dart:async';

import 'package:flutter_riverpod/flutter_riverpod.dart';

final countDownControllerProvider = StateNotifierProvider.family
    .autoDispose<CountdownController, Duration, Duration>((ref, initialDuration) {
  return CountdownController(initialDuration);
});

class CountdownController extends StateNotifier<Duration> {
  Timer? timer;
  final Duration initialDuration;

  CountdownController(this.initialDuration) : super(initialDuration) {
    startTimer();
  }

  void startTimer() {
    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      if (state == Duration.zero) {
        timer.cancel();
      } else {
        if (mounted) {
          state = state - const Duration(seconds: 1);
        } else {
          timer.cancel();
        }
      }
    });
  }

  void stopTimer() {
    timer?.cancel();
  }

  void resetTimer({required Duration initialDuration}) {
    stopTimer();
    state = initialDuration;
    startTimer();
  }

  void addTime({required Duration duration}) {
    state = state + duration;
  }

  void subtractTime({required Duration duration}) {
    state = state - duration;
  }

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

Then in the widget

Consumer(builder: (context, ref, _) {
                          return Text(
                            ref
                                .watch(countDownControllerProvider(
                                    const Duration(
                                        days: 25,
                                        hours: 15,
                                        minutes: 36,
                                        seconds: 45)))
                                .formatDuration(),
                            style: context.theme.textTheme.bodyText1!
                                .copyWith(color: Colors.white),
                          );
                        })

And finally, don't hesitate to put your conversion logic Duration to String, into a extension

extension DurationExtensions on Duration {
  String formatDuration() {
    int seconds = inSeconds;
    final days = seconds~/Duration.secondsPerDay;
    seconds -= days*Duration.secondsPerDay;
    final hours = seconds~/Duration.secondsPerHour;
    seconds -= hours*Duration.secondsPerHour;
    final minutes = seconds~/Duration.secondsPerMinute;
    seconds -= minutes*Duration.secondsPerMinute;

    final List<String> tokens = [];
    if (days != 0) {
      tokens.add('${days}d');
    }
    if (tokens.isNotEmpty || hours != 0){
      tokens.add('${hours}h');
    }
    if (tokens.isNotEmpty || minutes != 0) {
      tokens.add('${minutes}min');
    }
    if(tokens.isNotEmpty || seconds != 0) {
      tokens.add('${seconds}s');
    }

    return tokens.join('');
  }
}
mario francois
  • 1,291
  • 1
  • 9
  • 16
  • No need to introduce a dependency on Riverpod for the purpose of this question – jraufeisen Jan 23 '23 at 22:44
  • Riverpod is the evolution of Provider. It is highly recommanded. They almost think of replace Provider by Riverpod. At Least you know it's not the future. – mario francois Jan 24 '23 at 08:21
  • Thanks so much for the answer @mariofrancois. I'm currently getting my head around RiverPod now and am so close to what I need it to be. I do have a couple of follow up questions I was hoping you would be able to help me with please? first one is, how would I go about calling the start and stop time functions? i have made the stateful widget a ConsumerStatefulWidget but using ref.read(CountdownController.notifier).startTimer(); when the bool changes in the setstate is not working. – Nomad09 Jan 30 '23 at 15:08
  • ref.read(countDownControllerProvider(Duration.zero).notifier).addTime(duration: const Duration(seconds: 1)); With Family ref.read(countDownControllerProvider.notifier).subtractTime(duration: const Duration(seconds: 1)); Without family – mario francois Jan 30 '23 at 16:35
0

The trick you are looking for is the function notifyListeners() of a ChangeNotifier. If you used context.watch() inside your widget to watch the ChangeNotifier, you will be updated and the widget is rebuild if necessary.

A short example might look like this


ConsumingWidget.dart

@override
Widget build(BuildContext context) {
  var timeProvider = context.watch<TimeProvider>();
  int counter = timeProvider.counter;

  return Text("${counter} seconds have passed");
}

In this case, you would need to provide an instance of TimeProvider above your main widget in the widget tree via ChangeNotifierProvider. You can then let the widget rebuild (if .counter changed) via notifyListeners()


ParentWidget.dart

@override
Widget build(BuildContext context) {
  ChangeNotifierProvider(
    create: (_) => TimeProvider(),
    child: ConsumingWidget()
  );
}

Provider.dart

class TimeProvider extends ChangeNotifier {
  Timer? timer;
  final Duration initialDuration;
  int counter = 0;

  CountdownController(this.initialDuration) : super(initialDuration) {
    startTimer();
  }

  void startTimer() {
    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      counter += 1; // Change some data based on the timer. Just an example here
      notifyListeners(); // Then update listeners!
    });
  }

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

This is the built-in solution of the Provider package. Starting from this basic pattern, you can then look into more sophisticated state management solutions like Riverpod, Bloc etc.

jraufeisen
  • 3,005
  • 7
  • 27
  • 43