在构建过程中调用Flutter Provider setState()或markNeedsBuild()

时间:2019-12-17 16:22:27

标签: flutter dart flutter-provider

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

我正在尝试 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();
    }
}

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

2 个答案:

答案 0 :(得分:3)

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

现在,您可能正在考虑“但是async方法被标记为async,因此以后应该异步运行”。答案是“是与否”。 async在Dart中的工作方式是,当您调用async方法时,Dart尝试同步运行该方法中尽可能多的代码。简而言之,这意味着awaitfetchEvents方法中的所有代码都将作为常规同步代码运行。如果我们看一下您的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,因此将被同步调用。这意味着从小部件的build方法调用此方法,就好像您在build方法本身中调用了Future.delayed一样。

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

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

subprocess.run

答案 1 :(得分:1)

问题是您在一个函数中两次调用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();
}