简体   繁体   English

Flutter:查找已停用小部件的祖先是不安全的

[英]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.此代码不需要放在 StatefulWidget 中,但我尝试将其放在 StatefulWidget 中以解决问题,但没有成功。

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()我认为问题已通过Saman Salehi的解决方案解决,但在调试模式下工作时,我在 _fetchData function 中遇到了同样的异常,现在在 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 .应用Stewie Griffin建议的编辑后,我遇到了另一个错误。

The error is on the line Provider.of<DataProvider>(context, listen: false).init(initData);错误在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它似乎没有其他错误那么常见,所以 Stewie Griffin 的回答肯定提高了我的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?请你帮助我好吗?

First of all, never call async methods inside of build as mentioned.首先,永远不要像上面提到的那样在 build 内部调用异步方法。 Build method is constantly rebuilt and it will cause your fetch method to be repeated like an infinite loop. Build 方法会不断重建,它会导致您的 fetch 方法像无限循环一样重复。 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.您不应该在构建期间调用 Navigator。 Here is what you need to do:这是您需要做的:

Add this line to the top of your file to use SchedulerBinding :将此行添加到文件顶部以使用SchedulerBinding

import 'package:flutter/scheduler.dart';

Wrap Navigator with SchedulerBinding to wait for completing the state before navigating to another screen.使用 SchedulerBinding 包裹 Navigator 以等待完成 state,然后再导航到另一个屏幕。 Then, call your async method inside of initState .然后,在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.提示:您不需要在 Stateful Widget 中传递上下文,因为您可以从任何地方访问它。

You shouldn't use the build method for anything other than building UI.除了构建 UI 之外,您不应将build方法用于其他任何事情。 build can be called at any time even when it's not on the screen. build可以随时调用,即使它不在屏幕上。

I would move the _fetchData to the initState so it wouldn't cause any conflict at the build method.我会将_fetchData initState这样它就不会在build方法中引起任何冲突。

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);
  }
}
 Navigator.pushReplacement(context,MaterialPageRoute(builder:(context) => WelcomeScreen()),); 

|^|.wrap this code SchedulerBinding.instance..addPostFrameCallback on above code. |^|.wrap 此代码SchedulerBinding.instance..addPostFrameCallback在上面的代码上。 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:前:

在此处输入图像描述


After

在此处输入图像描述

SampleCode Here i use rive: ^0.8.1 dartpackage示例代码这里我使用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),
          ),
        ),
      ),
    );
  }
}

if it is caused by accessing ScaffoldMessenger by context then putting it inside a try catch will resolve context error.如果它是由上下文访问 ScaffoldMessenger 引起的,那么将其放入 try catch 将解决上下文错误。

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

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.但是如果您同时对 state 进行大量更改,此解决方案可能仍然会出现相同的错误。

if is the case try to use the second solution by augmenting the duration.如果是这种情况,请尝试通过增加持续时间来使用第二种解决方案。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM