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

Flutter Provider setState() or markNeedsBuild() called during build

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

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

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

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

════════ Exception caught by foundation library ════════

The following assertion was thrown while dispatching notifications for EventProvider:

setState() or markNeedsBuild() called during build.

...

The EventProvider sending notification was: Instance of 'EventProvider'

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

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

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

主要功能:

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

谁能解释一下发生了什么?

问题是您在一个函数中调用了两次 notifyListeners。我明白了,你想改变状态。但是,事件提供者应该 而不是 负责在应用程序加载时通知应用程序。您所要做的就是如果它没有加载,假设它正在加载,然后放一个 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();
}

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

此时,您可能会想 "but the fetchEvents method is marked as async so it should be running asynchronous for later"。答案是 "yes and no"。 async 在 Dart 中的工作方式是,当您调用 async 方法时,Dart 会尝试同步 运行 方法中尽可能多的代码。简而言之,这意味着 async 方法中位于 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();
}

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

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

您可以改为从 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();
}