构建期间调用了Flutter提供程序setState()或markNeedsBuild()

rqcrx0a6  于 2023-01-27  发布在  Flutter
关注(0)|答案(3)|浏览(164)

我想加载一个事件列表,并在获取数据时显示加载指示符。
我正在尝试Provider模式(实际上是重构现有的应用程序)。
因此,事件列表显示是有条件的,取决于提供程序中管理的状态。
问题是当我调用notifyListeners()的速度太快时,会出现以下异常:
基础库捕获到异常
为EventProvider调度通知时引发了以下Assert:
在构建期间调用了setState()或NeedmarksBuild()。
...
发送通知的EventProvider为:"事件提供程序"的示例
════════════════════════════════════════
在调用notifyListeners()解决问题之前等待几毫秒(参见下面provider类中的注解行)。
这是一个基于我的代码的简单示例(希望不要过于简化):

    • 主要功能**:
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();
    }
}

有人能解释一下发生了什么吗?

0md85ypi

0md85ypi1#

您正在从根小部件的构建代码中调用fetchEvents。在fetchEvents中,您调用notifyListenersnotifyListeners会在侦听事件提供程序的小部件上调用setState。这是一个问题,因为当小部件处于重建过程中时,您无法在小部件上调用setState
现在,您可能会想“但是fetchEvents方法被标记为async,所以它以后应该异步运行”。而对此的答案是“是和否”。async在Dart中的工作方式是,当您调用async方法时,Dart尝试同步运行该方法中尽可能多的代码。简单地说,这意味着async方法中任何在await之前的代码都将作为普通同步代码运行。

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的调用时,Flutter可以正常工作的原因是,现在在方法的顶部有一个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();
}
snz8szmq

snz8szmq2#

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

vq8itlhq3#

您将从根小部件的代码中调用API。在API中,您将调用notifyListeners,它将调用侦听事件提供程序的小部件上的setState。因此,首先删除代码中的setState,并确保在初始化状态下调用API时的Future use

@override
  void initState() {
    super.initState();
    Future.microtask(() => context.read<SellCarProvider>().getBrandService(context));
  }

相关问题