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

Yan*_*n39 22 dart flutter flutter-provider

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

我正在尝试提供者模式(实际上是重构一个现有的应用程序)。

因此,事件列表的显示取决于提供程序中管理的状态。

问题是当我打电话notifyListeners()太快时,我得到这个异常:

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

为 EventProvider 分派通知时抛出以下断言:

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

...

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

??????????????????????????????????????????

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

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

主要功能

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

根小部件

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();
            },
        );

    }
}
Run Code Online (Sandbox Code Playgroud)

主页

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(...)
        )
    }
}
Run Code Online (Sandbox Code Playgroud)

事件提供者

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Abi*_*n47 45

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

现在,您可能会想“但是该fetchEvents方法被标记为async应该异步运行以备后用”。答案是“是也不是”。asyncDart 的工作方式是,当你调用一个async方法时,Dart 尝试同步运行尽可能多的方法中的代码。简而言之,这意味着async方法中出现在 an 之前的任何代码await都将作为普通同步代码运行。如果我们看看你的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();
}
Run Code Online (Sandbox Code Playgroud)

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

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

您可以改为fetchEvents从该initState方法调用,但这会遇到另一个类似的问题:您也无法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();
}
Run Code Online (Sandbox Code Playgroud)

  • 老实说,我认为有一些内部处理会阻止“NotifyListeners()”在构建小部件时调用“setState()”(手动调用除外)。由于我的整个应用程序中只有无状态小部件,并且不调用任何“setState()”,因此我不明白。我的第一个想法是在“EventProvider”构造函数中获取事件,但我需要来自“LoginProvider”的用户电子邮件,并且即使在路由上传递构造函数参数也无法使其正常工作(给了我异常“(inheritFromWidgetOfExactType(_ModalScopeStatus)”或调用了inheritFromElement()`。 (3认同)
  • @buttonsrtoys 异步编程不应与多线程编程混淆。`async` 只是意味着该方法可以稍后完成,而不是说它将在不同的线程上完成。Dart是纯粹的单线程语言,所以你的情况不可能发生。会发生的情况是,当线程停机时(例如,在绘图帧之间),它会检查异步队列以查看是否有任何 future 已完成。如果有,则返回这些 future 的结果,并恢复“等待”它们的任何代码。在构建过程中发生这种情况的可能性为 0%。 (2认同)

Ben*_*min 6

问题是您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();
}
Run Code Online (Sandbox Code Playgroud)

  • 在一个方法中调用两次“notifyListeners”通常是多余的(尽管并非总是如此),但这样做并没有真正的不利影响。这不是OP错误的原因。 (3认同)