Flutter - Provider - Flutter_Countdown_Timer - CountdownTimerController - FlutterError (setState() or markNeedsBuild() called during build

Viewed 109

I am developing an app using Flutter, managing state with provider, and I keep running into the error:

"FlutterError (setState() or markNeedsBuild() called during build. This _InheritedProviderScope<CountdownTimerController?> widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase."

I'm using the package flutter countdown timer as a widget, but I am trying to place the associated CountdownTimerController as a provider higher in the widget tree so I can access its state from other widgets. Whenever I attempt to define a countdown timer widget using provider.watch, I receive the above error at compile time. Oddly, the widget will still build, but the error throws every time I try to add a new instance of the widget into a listview with an action button.

Here is my provider definition within a listview builder:

body: ListView.separated(
  padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
  itemCount: challenges.length,
  itemBuilder: (BuildContext context, int index) {
    Challenge challenge = challenges[index];
    return MultiProvider(providers: [
      ChangeNotifierProvider(
          create: (context) => CountdownTimerController(
              endTime: challenge.nextCheckIn.millisecondsSinceEpoch))
    ], child: ChallengeCard(challenge));
  },

Here is my custom ChallengeCard widget, where I am attempting to access the controller with context.watch:

class ChallengeCard extends StatefulWidget {
  final Challenge challenge;
  const ChallengeCard(this.challenge, {Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _ChallengeCardState();
}

class _ChallengeCardState extends State<ChallengeCard> {
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
        child: Column(
          children: [
            Row(
              children: [
                Expanded(
                  flex: 3,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        widget.challenge.passage.fullRef,
                        style: TextStyles.title,
                      ),
                      Row(
                        children: [
                          const Text(
                            "Next Check In: ",
                            style: TextStyles.subtitle,
                          ),
                          CountdownTimer(
                            textStyle: TextStyles.subtitle,
                            endWidget: const Text(
                              "Now!",
                              style: TextStyles.subtitle,
                            ),
                            controller:
                                context.watch<CountdownTimerController>(),
                            onEnd: () {
                              print("Timer Finished!");
                            },
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
                Expanded(child: DetailsButton(widget.challenge)),
                Expanded(child: CheckInButton(widget.challenge)),
              ],
            ),
            CounterProgressIndicator(widget.challenge)
          ],
        ),
      ),
    );
  }
}

Here is the full flutter_countdown_provider class:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_countdown_timer/index.dart';

typedef CountdownTimerWidgetBuilder = Widget Function(
    BuildContext context, CurrentRemainingTime? time);

/// A Countdown.
class CountdownTimer extends StatefulWidget {
  ///Widget displayed after the countdown
  final Widget endWidget;
  ///Used to customize the countdown style widget
  final CountdownTimerWidgetBuilder? widgetBuilder;
  ///Countdown controller, can end the countdown event early
  final CountdownTimerController? controller;
  ///Countdown text style
  final TextStyle? textStyle;
  ///Event called after the countdown ends
  final VoidCallback? onEnd;
  ///The end time of the countdown.
  final int? endTime;

  CountdownTimer({
    Key? key,
    this.endWidget = const Center(
      child: Text('The current time has expired'),
    ),
    this.widgetBuilder,
    this.controller,
    this.textStyle,
    this.endTime,
    this.onEnd,
  })  : assert(endTime != null || controller != null),
        super(key: key);

  @override
  _CountDownState createState() => _CountDownState();
}

class _CountDownState extends State<CountdownTimer> {
  late CountdownTimerController controller;

  CurrentRemainingTime? get currentRemainingTime =>
      controller.currentRemainingTime;

  Widget get endWidget => widget.endWidget;

  CountdownTimerWidgetBuilder get widgetBuilder =>
      widget.widgetBuilder ?? builderCountdownTimer;

  TextStyle? get textStyle => widget.textStyle;

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

  ///Generate countdown controller.
  initController() {
    controller = widget.controller ??
        CountdownTimerController(endTime: widget.endTime!, onEnd: widget.onEnd);
    if (controller.isRunning == false) {
      controller.start();
    }
    controller.addListener(() {
      if (mounted) {
        setState(() {});
      }
    });
  }

  @override
  void didUpdateWidget(CountdownTimer oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.endTime != widget.endTime || widget.controller != oldWidget.controller) {
      controller.dispose();
      initController();
    }
  }

  @override
  Widget build(BuildContext context) {
    return widgetBuilder(context, currentRemainingTime);
  }

  Widget builderCountdownTimer(
      BuildContext context, CurrentRemainingTime? time) {
    if (time == null) {
      return endWidget;
    }
    String value = '';
    if (time.days != null) {
      var days = time.days!;
      value = '$value$days days ';
    }
    var hours = _getNumberAddZero(time.hours ?? 0);
    value = '$value$hours : ';
    var min = _getNumberAddZero(time.min ?? 0);
    value = '$value$min : ';
    var sec = _getNumberAddZero(time.sec ?? 0);
    value = '$value$sec';
    return Text(
      value,
      style: textStyle,
    );
  }

  /// 1 -> 01
  String _getNumberAddZero(int number) {
    if (number < 10) {
      return "0" + number.toString();
    }
    return number.toString();
  }
}

Here is the full CountdownTimerController class:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_countdown_timer/index.dart';

///Countdown timer controller.
class CountdownTimerController extends ChangeNotifier {
  CountdownTimerController(
      {required int endTime, this.onEnd, TickerProvider? vsync})
      : this._endTime = endTime {
    if (vsync != null) {
      this._animationController =
          AnimationController(vsync: vsync, duration: Duration(seconds: 1));
    }
  }

  ///Event called after the countdown ends
  final VoidCallback? onEnd;

  ///The end time of the countdown.
  int _endTime;

  ///Is the countdown running.
  bool _isRunning = false;

  ///Countdown remaining time.
  CurrentRemainingTime? _currentRemainingTime;

  ///Countdown timer.
  Timer? _countdownTimer;

  ///Intervals.
  Duration intervals = const Duration(seconds: 1);

  ///Seconds in a day
  int _daySecond = 60 * 60 * 24;

  ///Seconds in an hour
  int _hourSecond = 60 * 60;

  ///Seconds in a minute
  int _minuteSecond = 60;

  bool get isRunning => _isRunning;

  set endTime(int endTime) => _endTime = endTime;

  ///Get the current remaining time
  CurrentRemainingTime? get currentRemainingTime => _currentRemainingTime;

  AnimationController? _animationController;

  ///Start countdown
  start() {
    disposeTimer();
    _isRunning = true;
    _countdownPeriodicEvent();
    if (_isRunning) {
      _countdownTimer = Timer.periodic(intervals, (timer) {
        _countdownPeriodicEvent();
      });
    }
  }

  ///Check if the countdown is over and issue a notification.
  _countdownPeriodicEvent() {
    _currentRemainingTime = _calculateCurrentRemainingTime();
    _animationController?.reverse(from: 1);
    notifyListeners();
    if (_currentRemainingTime == null) {
      onEnd?.call();
      disposeTimer();
    }
  }

  ///Calculate current remaining time.
  CurrentRemainingTime? _calculateCurrentRemainingTime() {
    int remainingTimeStamp =
        ((_endTime - DateTime.now().millisecondsSinceEpoch) / 1000).floor();
    if (remainingTimeStamp <= 0) {
      return null;
    }
    int? days, hours, min, sec;

    ///Calculate the number of days remaining.
    if (remainingTimeStamp >= _daySecond) {
      days = (remainingTimeStamp / _daySecond).floor();
      remainingTimeStamp -= days * _daySecond;
    }

    ///Calculate remaining hours.
    if (remainingTimeStamp >= _hourSecond) {
      hours = (remainingTimeStamp / _hourSecond).floor();
      remainingTimeStamp -= hours * _hourSecond;
    } else if (days != null) {
      hours = 0;
    }

    ///Calculate remaining minutes.
    if (remainingTimeStamp >= _minuteSecond) {
      min = (remainingTimeStamp / _minuteSecond).floor();
      remainingTimeStamp -= min * _minuteSecond;
    } else if (hours != null) {
      min = 0;
    }

    ///Calculate remaining second.
    sec = remainingTimeStamp.toInt();
    return CurrentRemainingTime(
        days: days,
        hours: hours,
        min: min,
        sec: sec,
        milliseconds: _animationController?.view);
  }

  disposeTimer() {
    _isRunning = false;
    _countdownTimer?.cancel();
    _countdownTimer = null;
  }

  @override
  void dispose() {
    disposeTimer();
    _animationController?.dispose();
    super.dispose();
  }
}

0 Answers
Related