Draxent
Draxent

Reputation: 520

Flutter: Looking up a deactivated widget's ancestor is unsafe


I know that there are already two posts regarding this question, but I could not solve my issue looking at them. Probably because in my case the issue is different.

The code is the following. I want to load some data from the database while showing a loading page. After the data is loaded, I initialize the provider with the loaded data and then I move into a different page. This code does not need to be in a StatefulWidget, but I try to put it in a StatefulWidget to solve the issue, but without success.

class _InitDBDataState extends State<_InitDBData> {
  @override
  Widget build(BuildContext context) {
    _fetchData(context);
    return const Center(child: const CircularProgressIndicator());
  }

  Future<void> _fetchData(BuildContext context) async {
    print('fetching data...');
    print('context: $context');
    final initData = await DBService.service.getInitialData();
    print('Data fetched');
    print('context: $context');
    Provider.of<DataProvider>(context, listen: false).init(initData);
    Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
  }
}

I do not have any error if the application runs from scratch, but when I am doing a "Hot Reload" I get the following error pretty often, and it is annoying since I need to restart the application for each small change in the code.

I/flutter ( 9596): fetching data...
I/flutter ( 9596): context: _InitDBData(dirty, state: _InitDBDataState#46860)
I/flutter ( 9596): fetching data...
I/flutter ( 9596): context: _InitDBData(dirty, state: _InitDBDataState#55124)
I/flutter ( 9596): Data fetched
I/flutter ( 9596): context: _InitDBData
E/flutter ( 9596): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
E/flutter ( 9596): At this point the state of the widget's element tree is no longer stable.
E/flutter ( 9596): 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.
E/flutter ( 9596): #0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> 
package:flutter/…/widgets/framework.dart:3508
E/flutter ( 9596): #1      Element._debugCheckStateIsActiveForAncestorLookup 
package:flutter/…/widgets/framework.dart:3522
E/flutter ( 9596): #2      Element.getElementForInheritedWidgetOfExactType 
package:flutter/…/widgets/framework.dart:3588
E/flutter ( 9596): #3      Provider.of 
package:provider/src/provider.dart:221
E/flutter ( 9596): #4      _InitDBDataState._fetchData 
package:productive_diary/initScreen.dart:46
E/flutter ( 9596): <asynchronous suspension>
E/flutter ( 9596): #5      _InitDBDataState.build 

I don't know why "fetching data..." is printed twice, and I have no clue on how to solve the issue.


I thought the issue was solved with the solution of Saman Salehi, but working in debug mode I had the same exception in the _fetchData function, that now is called in the function initState()

Exception has occurred.
FlutterError (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.)

I got another error after applying the edits suggested by Stewie Griffin .

The error is on the line Provider.of<DataProvider>(context, listen: false).init(initData);

I got it during a hot reload. It seems less common than the other error, so the answer of Stewie Griffin surely improved the stability of my Stewie Griffin

E/flutter (23815): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: NoSuchMethodError: The getter 'owner' was called on null.
E/flutter (23815): Receiver: null
E/flutter (23815): Tried calling: owner
E/flutter (23815): #0      Object.noSuchMethod  (dart:core-patch/object_patch.dart:53:5)
E/flutter (23815): #1      Provider.of 
package:provider/src/provider.dart:193
E/flutter (23815): #2      _InitDBDataState._fetchData 
package:productive_diary/initScreen.dart:49
E/flutter (23815): <asynchronous suspension>
E/flutter (23815): #3      _InitDBDataState.initState 

Could you please help me?

Upvotes: 23

Views: 64774

Answers (5)

Ahmed El Rhaouti
Ahmed El Rhaouti

Reputation: 1857

For this probleme you can use one of this two solutions : first: add scheduler like this :

 SchedulerBinding.instance!.addPostFrameCallback((_) {
   Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
});

second: add future delayed with some milliseconds like this :

  Future.delayed(Duration(microseconds: 200))
                            .then((value) {
   Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
});

!!! But if you a lot of change of state in the same time this solutions may still give the same error.

if is the case try to use the second solution by augmenting the duration.

Upvotes: 0

A.K.J.94
A.K.J.94

Reputation: 552

if it is caused by accessing ScaffoldMessenger by context then putting it inside a try catch will resolve context error.

try{
ScaffoldMessenger.of(_scaffoldKey.currentContext!).showSnackBar();
} catch(e){
print(e);
}

Upvotes: -1

lava
lava

Reputation: 7451

 Navigator.pushReplacement(context,MaterialPageRoute(builder:(context) => WelcomeScreen()),); 

|^| .wrap this code SchedulerBinding.instance!.addPostFrameCallback on above code. like this below:

SchedulerBinding.instance!.addPostFrameCallback((_) {
  Navigator.pushReplacement(
    context,
    MaterialPageRoute(builder: (context) => WelcomeScreen()),
  );
});

Schedule a callback for the end of this frame.

Does not request a new frame.

This callback is run during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven't been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.

The callbacks are executed in the order in which they have been added.

Post-frame callbacks cannot be unregistered. They are called exactly once.

Before:

enter image description here


After

enter image description here

SampleCode Here i use rive: ^0.8.1 dartpackage

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:rive/rive.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(MaterialApp(home: SimpleAnimation()));
}

setdata(BuildContext context) async {
  await Future.delayed(const Duration(seconds: 5), () {
    SchedulerBinding.instance!.addPostFrameCallback((_) {
      Navigator.pushReplacement(
        context,
        MaterialPageRoute(builder: (context) => WelcomeScreen()),
      );
    });

  });
}

class SimpleAnimation extends StatelessWidget {
  const SimpleAnimation({Key? key, this.loading}) : super(key: key);
  final bool? loading;

  @override
  Widget build(BuildContext context) {
    setdata(context);
    return Scaffold(
      body: Center(
        child: Container(
          height: 200,
          width: 200,
          child: RiveAnimation.network(
            'https://cdn.rive.app/animations/vehicles.riv',
          ),
        ),
      ),
    );
  }
}

class WelcomeScreen extends StatelessWidget {
  const WelcomeScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Container(
          child: Text(
            "HOME PAGE",
            style: TextStyle(fontSize: 50),
          ),
        ),
      ),
    );
  }
}

Upvotes: 0

Stewie Griffin
Stewie Griffin

Reputation: 5638

First of all, never call async methods inside of build as mentioned. Build method is constantly rebuilt and it will cause your fetch method to be repeated like an infinite loop. After you fix that, you still get the same error because of this part:

Navigator.of(context).pushReplacementNamed(MainScreen.routeName);

You shouldn't call Navigator during build. Here is what you need to do:

Add this line to the top of your file to use SchedulerBinding:

import 'package:flutter/scheduler.dart';

Wrap Navigator with SchedulerBinding to wait for completing the state before navigating to another screen. Then, call your async method inside of initState.

class _InitDBDataState extends State<_InitDBData> {
@override
  void initState() {
    // Call your async method here
    _fetchData();
    super.initState();
  }

  Future<void> _fetchData() async {
    print('fetching data...');
    print('context: $context');
    final initData = await DBService.service.getInitialData();
    print('Data fetched');
    print('context: $context');
    Provider.of<DataProvider>(context, listen: false).init(initData);

    // Wrap Navigator with SchedulerBinding to wait for rendering state before navigating
    SchedulerBinding.instance.addPostFrameCallback((_) {
      Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
    });
  }
  @override
  Widget build(BuildContext context) {
    return Center(child: CircularProgressIndicator());
  }
}

Tip: You don't need to pass the context in a Stateful Widget because you can access it from everywhere.

Upvotes: 51

Saman Salehi
Saman Salehi

Reputation: 2179

You shouldn't use the build method for anything other than building UI. build can be called at any time even when it's not on the screen.

I would move the _fetchData to the initState so it wouldn't cause any conflict at the build method.

class _InitDBDataState extends State<_InitDBData> {

  @override
  void initState() {
    super.initState();
    _fetchData(context);
  }

  @override
  Widget build(BuildContext context) {
    return const Center(child: const CircularProgressIndicator());
  }

  Future<void> _fetchData(BuildContext context) async {
    print('fetching data...');
    print('context: $context');
    final initData = await DBService.service.getInitialData();
    print('Data fetched');
    print('context: $context');
    Provider.of<DataProvider>(context, listen: false).init(initData);
    Navigator.of(context).pushReplacementNamed(MainScreen.routeName);
  }
}

Upvotes: 7

Related Questions