Search code examples
flutternavigationflutter-dependenciesflutter-state

To safely refer to a widget's ancestor in its dispose() method


I want to build a math app with Flutter. It should have basic functions. However, I am facing the following issue: when my timer runs out and I am directed to the next page, the following error message appears:

The following assertion was thrown while finalizing the widget tree: Looking up a deactivated widget's ancestor is unsafe.

At this point the state of the widget's element tree is no longer stable.

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

class MathFunctionScreen extends StatefulWidget {
  static String id = "MathFunctionScreen";
  const MathFunctionScreen({Key? key}) : super(key: key);
  @override
  State<MathFunctionScreen> createState() => _MathFunctionScreenState();
}

class _MathFunctionScreenState extends State<MathFunctionScreen>
    with TickerProviderStateMixin {
  DataBase db = DataBase();
  late int myTime;
  late int timeStamp;
  @override
  void initState() {
    print("initState() wurde ausgeführt");
    Provider.of<NumberGenerator>(context, listen: false).ctrl;
    myOnChange();
    Provider.of<NumberGenerator>(context, listen: false).mathCorrectAnswer;
    Provider.of<NumberGenerator>(context, listen: false).operator;
    Provider.of<NumberGenerator>(context, listen: false).setDatabase();
    Provider.of<TimerProvider>(context, listen: false).startTimer(context);
    animationController;
    offsetAnimation;
    animationController.forward();
    super.initState();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    Provider.of<TimerProvider>(context, listen: false).timer.cancel();
    Provider.of<NumberGenerator>(context, listen: false)
        .ctrl
        .removeListener(() {});
    animationController.dispose();
    super.dispose();
  }

  late AnimationController animationController = AnimationController(
    vsync: this,
    duration: const Duration(milliseconds: 600),
  );
  late Animation<Offset> offsetAnimation =
      Tween<Offset>(begin: const Offset(-3, 0), end: Offset.zero).animate(
    CurvedAnimation(parent: animationController, curve: Curves.easeIn),
  );
  void myOnChange() {
    Provider.of<NumberGenerator>(context, listen: false)
        .textFieldTextChange(animationController);
  }

  void _updateText(String text) async {
    String answer = (await Provider.of<NumberGenerator>(context, listen: false)
            .mathCorrectAnswer)
        .toString();
    int answerLength = answer.length;
    setState(() {
      if (Provider.of<NumberGenerator>(context, listen: false)
              .ctrl
              .text
              .length <
          answerLength) {
        Provider.of<NumberGenerator>(context, listen: false).ctrl.text =
            Provider.of<NumberGenerator>(context, listen: false).ctrl.text +
                text;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    double height = MediaQuery.of(context).size.height;
    double width = MediaQuery.of(context).size.width;

And the following method is in a class called NumberGenerator, which inherits ChangeNotifier

void textFieldTextChange(AnimationController animationController) {
    ctrl.addListener(
      () async {
        String mathCorrectAnswerString = (await mathCorrectAnswer).toString();
        // user hat die zulässige Länge erreicht
        if (ctrl.text.length >= mathCorrectAnswerString.length) {
          //this delayed ist, damit die letzte zahl noch gesehen werden kann!
          await Future.delayed(const Duration(milliseconds: 200));
          checkAnswer(ctrl.text == mathCorrectAnswerString);
          if (ctrl.text == mathCorrectAnswerString) {
            updateDatabaseResult();
            await Future.delayed(const Duration(milliseconds: 20));
            wrongAnswers.clear();
            wrongAnswers.add(addRightIcon);
            notifyListeners();
            await Future.delayed(const Duration(milliseconds: 500));
            wrongAnswers.clear();
            updateLeftAndRightInt();
            setDatabase();
            animationController.reset();
            animationController.forward();
            notifyListeners();
          } else if (ctrl.text != mathCorrectAnswerString &&
              (wrongAnswers.length <= 1)) {
            updateDatabaseResult();
            wrongAnswers.add(addWrongIcon);
            notifyListeners();
            await Future.delayed(const Duration(milliseconds: 500));
            ctrl.clear();
          } else {
            updateDatabaseResult();
            wrongAnswers.add(addWrongIcon);
            notifyListeners();
            await Future.delayed(const Duration(seconds: 1));
            updateLeftAndRightInt();
            setDatabase();
            animationController.reset();
            animationController.forward();
            wrongAnswers.clear();
            notifyListeners();
          }
        }

And the following class contains the timer. When it has expired, I am redirected to the new page and receive the error message:

class TimerProvider extends ChangeNotifier {
  late Timer timer;
  late int value;
  String collectionTime = "Time";
  String key = "myTime";
  startTimer(BuildContext context) {
    timer = Timer.periodic(
      const Duration(seconds: 1),
      (timer) async {
        if (value > 0) {
          value--;
          notifyListeners();
        } else if (value <= 0) {
          final QuerySnapshot<Map<String, dynamic>> documents =
              await FirebaseFirestore.instance
                  .collection('Funktionen')
                  .where('result', isEqualTo: "")
                  .get();
          final List<Future<void>> deleteFutures = [];
          for (final QueryDocumentSnapshot<Map<String, dynamic>> document
              in documents.docs) {
            deleteFutures.add(document.reference.delete());
          }
          await Future.wait(deleteFutures);
          timer.cancel();
          if (context.mounted) {
            Navigator.pushReplacementNamed(context, UserResultScreen.id);
            // Navigator.pushNamed(context, UserResultScreen.id);
          }
        }
      },
    );
  }

Can someone help me with this ?


Solution

  • It`s because you called the inherited widget inside dispose of, in you're sample this is Provider.of<"Type">

    You need to do this way:

    class _MathFunctionScreenState extends State<MathFunctionScreen>
      with TickerProviderStateMixin {
      ...
      NumberGenerator numberGenerator;
      TimerProvider timerProvider;
      ...
    
      @override
      void didChangeDependencies() {
        numberGenerator = Provider.of<NumberGenerator>;
        timerProvider = Provider.of<TimerProvider>;
        super.didChangeDependencies();
      }
    
      @override
      void dispose() {
        timerProvider.timer.cancel();
        numberGenerator.ctrl.removeListener(() {});
        animationController.dispose();
        super.dispose();
      }
    }