【问题标题】:Flutter Provider setState() or markNeedsBuild() called during build构建期间调用的 Flutter Provider setState() 或 markNeedsBuild()
【发布时间】:2020-04-10 04:39:30
【问题描述】:

我想在获取数据时加载事件列表并显示加载指示器。

我正在尝试 Provider 模式(实际上是重构现有应用程序)。

因此,事件列表显示是根据提供程序中管理的状态有条件的。

问题是当我太快调用notifyListeners() 时,我得到了这个异常:

════════基础库捕获的异常════════

在为 EventProvider 发送通知时抛出以下断言:

在构建期间调用 setState() 或 markNeedsBuild()。

...

EventProvider 发送通知是:“EventProvider”的实例

════════════════════════════════════════

在调用notifyListeners() 之前等待几毫秒来解决问题(请参阅下面提供程序类中的注释行)。

这是一个基于我的代码的简单示例(希望不要过于简化):

主要功能

Future<void> main() async {
    runApp(
        MultiProvider(
            providers: [
                ChangeNotifierProvider(create: (_) => LoginProvider()),
                ChangeNotifierProvider(create: (_) => EventProvider()),
            ],
            child: MyApp(),
        ),
    );
}

根小部件

class MyApp extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
        final LoginProvider _loginProvider = Provider.of<LoginProvider>(context, listen: true);
        final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: false);

        // load user events when user is logged
        if (_loginProvider.loggedUser != null) {
            _eventProvider.fetchEvents(_loginProvider.loggedUser.email);
        }

        return MaterialApp(
            home: switch (_loginProvider.status) { 
                case AuthStatus.Unauthenticated:
                    return MyLoginPage();
                case AuthStatus.Authenticated:
                    return MyHomePage();
            },
        );

    }
}

主页

class MyHomePage extends StatelessWidget {

    @override
    Widget build(BuildContext context) {
        final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: true);

        return Scaffold(
            body: _eventProvider.status == EventLoadingStatus.Loading ? CircularProgressIndicator() : ListView.builder(...)
        )
    }
}

事件提供者

enum EventLoadingStatus { NotLoaded, Loading, Loaded }

class EventProvider extends ChangeNotifier {

    final List<Event> _events = [];
    EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.NotLoaded;

    EventLoadingStatus get status => _eventLoadingStatus;

    Future<void> fetchEvents(String email) async {
        //await Future.delayed(const Duration(milliseconds: 100), (){});
        _eventLoadingStatus = EventLoadingStatus.Loading;
        notifyListeners();
        List<Event> events = await EventService().getEventsByUser(email);
        _events.clear();
        _events.addAll(events);
        _eventLoadingStatus = EventLoadingStatus.Loaded;
        notifyListeners();
    }
}

有人可以解释发生了什么吗?

【问题讨论】:

    标签: flutter dart flutter-provider


    【解决方案1】:

    您从根小部件的构建代码中调用fetchEvents。在fetchEvents 中,您调用notifyListeners,除其他外,它在侦听事件提供程序的小部件上调用setState。这是一个问题,因为当小部件处于重建过程中时,您无法在小部件上调用 setState

    此时,您可能会想“但fetchEvents 方法被标记为async,因此它应该在以后异步运行”。答案是“是与否”。 async 在 Dart 中的工作方式是,当您调用 async 方法时,Dart 会尝试同步运行方法中尽可能多的代码。简而言之,这意味着在 await 之前出现的 async 方法中的任何代码都将作为正常的同步代码运行。如果我们看看你的fetchEvents 方法:

    Future<void> fetchEvents(String email) async {
      //await Future.delayed(const Duration(milliseconds: 100), (){});
    
      _eventLoadingStatus = EventLoadingStatus.Loading;
    
      notifyListeners();
    
      List<Event> events = await EventService().getEventsByUser(email);
      _events.clear();
      _events.addAll(events);
      _eventLoadingStatus = EventLoadingStatus.Loaded;
    
      notifyListeners();
    }
    

    我们可以看到第一个await 发生在对EventService().getEventsByUser(email) 的调用中。在此之前有一个notifyListeners,所以它将被同步调用。这意味着从小部件的 build 方法调用此方法就像您在 build 方法本身中调用 notifyListeners 一样,正如我所说,这是被禁止的。

    当你添加对Future.delayed的调用时它起作用的原因是因为现在方法顶部有一个await,导致它下面的所有东西都异步运行。一旦执行到调用 notifyListeners 的代码部分,Flutter 就不再处于重建小部件的状态,因此此时调用该方法是安全的。

    您可以改为从initState 方法调用fetchEvents,但这会遇到另一个类似的问题:您也不能在小部件初始化之前调用setState

    那么,解决方案就是这样。与其通知所有监听事件提供者它正在加载的小部件,不如让它在创建时默认加载。 (这很好,因为它在创建后做的第一件事就是加载所有事件,所以不应该出现在第一次创建时不需要加载它的情况。)这消除了将提供者标记为的需要在方法的开头加载,这反过来又消除了在那里调用notifyListeners的需要:

    EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.Loading;
    
    ...
    
    Future<void> fetchEvents(String email) async {
      List<Event> events = await EventService().getEventsByUser(email);
      _events.clear();
      _events.addAll(events);
      _eventLoadingStatus = EventLoadingStatus.Loaded;
    
      notifyListeners();
    }
    

    【讨论】:

    • 老实说,我认为有一些内部处理会阻止NotifyListeners() 在构建小部件时调用setState()(除非手动调用)。由于我的整个应用程序中只有无状态小部件,并且不调用任何setState(),我不明白。我的第一个虽然是在EventProvider 构造函数中获取事件,但我需要来自LoginProvider 的用户电子邮件,并且即使在路由上传递构造函数参数也没有设法让它工作(给了我异常(inheritFromWidgetOfExactType(_ModalScopeStatus) or inheritFromElement() was called。跨度>
    • 这就是我在根小部件中结束调用fetchEvents 的原因。但是我只是看到ProxyProvider 在这种情况下可能会有所帮助,我必须试一试。但是对于这种特定情况,我可以假设它在按照您和本杰明的建议创建时默认加载。无论如何,这正是我正在寻找的答案,现在更清楚了,谢谢!
    • 我遇到了完全相同的问题,这个解决方案对我有用,但我很困惑为什么,特别是关于“所以它应该稍后异步运行”的评论。我没有做太多异步编程,但是异步函数不能随时运行?换句话说,构建线程不能在构建过程中换出并调用异步 notifyListeners,从而导致相同的错误吗?
    • @buttonsrtoys 异步编程不应与多线程编程相混淆。 async 只是意味着该方法可以稍后完成,而不是它将在不同的线程上完成。 Dart 是纯粹的单线程语言,所以你的情况不会发生。会发生的情况是,当线程停机时(例如,在绘图帧之间),它会检查异步队列以查看是否有任何期货已完成。如果有,则返回这些期货的结果,并恢复任何 awaiting 它们的代码。在构建过程中发生这种情况的可能性为 0%。
    【解决方案2】:

    问题是您在一个函数中调用了两次notifyListeners。我明白了,你想改变状态。但是,在加载时通​​知应用程序不应由 EventProvider 负责。你所要做的就是如果它没有加载,假设它正在加载,然后放一个CircularProgressIndicator。不要在同一个函数中调用notifyListeners 两次,这对你没有任何好处。

    如果你真的想这样做,试试这个:

    Future<void> fetchEvents(String email) async {
        markAsLoading();
        List<Event> events = await EventService().getEventsByUser(email);
        _events.clear();
        _events.addAll(events);
        _eventLoadingStatus = EventLoadingStatus.Loaded;
        notifyListeners();
    }
    
    void markAsLoading() {
        _eventLoadingStatus = EventLoadingStatus.Loading;
        notifyListeners();
    }
    

    【讨论】:

    • 在一种方法中调用notifyListeners 两次通常是多余的(尽管并非总是如此),但这样做并没有真正的不利影响。这不是 OP 错误的原因。
    • Abion47 是对的,我还有其他方法调用notifyListeners() 两次没有任何问题。在这种情况下,如果我在第一次通知之前添加一个等待,那么我可以多次调用notifyListeners() 而不会出现任何问题。但是你的答案的最终结果是正确的,我可以简单地假设它默认加载。
    • 在 markAsLoading 方法中调用 notifyListeners() 会改变什么吗?在我看来是一样的。
    • @funder7 不,它不会改变任何东西。将第二次调用notifyListeners 移动到另一个函数中并不会改变两个调用都在fetchEvents 范围内发生的事实。虽然就像我之前说的那样,像这样快速连续两次调用notifyListeners,虽然通常是多余的,但它本身是相对无害的。
    • 好的,我更仔细地准备了你的答案,我认为你是对的。将 UI 逻辑委托给 Provider 看起来不是一个好主意。阅读一些有关如何为此类目标集成 Provider 的实际示例会很不错。有什么建议吗?谢谢
    猜你喜欢
    • 2020-08-11
    • 2020-06-29
    • 2020-12-12
    • 2020-12-28
    • 2021-01-06
    • 2021-05-31
    • 2021-08-24
    相关资源
    最近更新 更多