Flutter Provider setState() or markNeedsBuild() called during build
Asked Answered
P

3

40

I want to load a list of events and display a loading indicator while fetching data.

I'm trying Provider pattern (actually refactoring an existing application).

So the event list display is conditional according to a status managed in the provider.

Problem is when I make a call to notifyListeners() too quickly, I get this exception :

════════ 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'

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

Waiting for some milliseconds before calling notifyListeners() solve the problem (see commented line in the provider class below).

This is a simple example based on my code (hope not over simplified) :

main function :

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

root widget :

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

    }
}

home page :

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

event provider :

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

Can someone explain what happens?

Pug answered 17/12, 2019 at 16:22 Comment(0)
K
70

You are calling fetchEvents from within your build code for the root widget. Within fetchEvents, you call notifyListeners, which, among other things, calls setState on widgets that are listening to the event provider. This is a problem because you cannot call setState on a widget when the widget is in the middle of rebuilding.

Now at this point, you might be thinking "but the fetchEvents method is marked as async so it should be running asynchronous for later". And the answer to that is "yes and no". The way async works in Dart is that when you call an async method, Dart attempts to run as much of the code in the method as possible synchronously. In a nutshell, that means any code in your async method that comes before an await is going to get run as normal synchronous code. If we take a look at your fetchEvents method:

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

We can see that the first await happens at the call to EventService().getEventsByUser(email). There's a notifyListeners before that, so that is going to get called synchronously. Which means calling this method from the build method of a widget will be as though you called notifyListeners in the build method itself, which as I've said, is forbidden.

The reason why it works when you add the call to Future.delayed is because now there is an await at the top of the method, causing everything underneath it to run asynchronously. Once the execution gets to the part of the code that calls notifyListeners, Flutter is no longer in a state of rebuilding widgets, so it is safe to call that method at that point.

You could instead call fetchEvents from the initState method, but that runs into another similar issue: you also can't call setState before the widget has been initialized.

The solution, then, is this. Instead of notifying all the widgets listening to the event provider that it is loading, have it be loading by default when it is created. (This is fine since the first thing it does after being created is load all the events, so there shouldn't ever be a scenario where it needs to not be loading when it's first created.) This eliminates the need to mark the provider as loading at the start of the method, which in turn eliminates the need to call notifyListeners there:

EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.Loading;

// or

late EventLoadingStatus _eventLoadingStatus;

@override
void initState() {
  super.initState();
  _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();
}
Kuntz answered 17/12, 2019 at 17:3 Comment(9)
To be honest I thought there was some internal processing that would prevent NotifyListeners() to call setState() while widget is building (except from being called manually). As I have only stateless widgets in my whole app, and do not call any setState(), I did not understand. My first though was to fetch events in the EventProvider constructor, but I need the user email from the LoginProvider and have not managed to get it to work even passing constructor argument on routing (gave me exception (inheritFromWidgetOfExactType(_ModalScopeStatus) or inheritFromElement() was called.Pug
This is why I ended calling fetchEvents in my root widget. However I just see that ProxyProvider could help in this case, I have to give it a try. But for that specific case I could just assume it is loading by default when it is created as you and Benjamin suggested. Anyway that is exactly the kind of answer I was looking for, this is much more clear now, thanks!Pug
I ran into the exact same problem and this solution worked for me but I'm mystified why, specifically about the comment "so it should be running asynchronous for later." I haven't done much async programming, but can't async function run anytime? In other words, couldn't the build thread swap out in the middle of a build and the async notifyListeners get called, resulting in the same error?Defrayal
@Defrayal Asynchronous programming shouldn't be confused with multi-threaded programming. async just means the method can be finished later, not that it will be finished on a different thread. Dart is purely a single-threaded language, so your situation can't happen. What would happen is that when the thread has downtime (between drawing frames, for instance), it checks the async queue to see if any futures have completed. If they have, the results of those futures are returned and any code that was awaiting them is resumed. There's 0% chance of this happening in the middle of a build.Kuntz
its only run once , when i click button to re fetch , the eventState doesnt watch or change ... alr put to set loading ...Warrant
so i need 1 void again to set only loading to true with notifylistener , ``` // gestureDetector context.read<ModelBrain>().eventLoading(); // need added this one , if i didnt put like these , the state doesnt change... context.read<ModelBrain>().getData(); ``` ``` initFetch(){ context.read<ModelBrain>().getData(); // } ``` and now i fix setState() or markNeedsBuild() called during buildWarrant
still not solve , i have listen from consumer then in consumer i need set marked loading = true . because have an event that trigger need to re fetch data.Warrant
@YogiArifWidodo I suggest posting your problem in a new question so you can share your code in a more readable and sharable format. You aren't likely to get good answers by cramming everything into the comment section of a three-year-old answer.Kuntz
@Kuntz ya alr created a new question, and alr solved .Warrant
A
8

The issue is you calling notifyListeners twice in one function. I get it, you want to change the state. However, it should not be the responsibility of the EventProvider to notify the app when it's loading. All you have to do is if it's not loaded, assume that it's loading and just put a CircularProgressIndicator. Don't call notifyListeners twice in the same function, it doesn't do you any good.

If you really want to do it, try this:

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();
}
Andrea answered 17/12, 2019 at 16:37 Comment(5)
Calling notifyListeners twice in one method is usually redundant (though not always), but there's no real adverse effect in doing it. This is not the cause of OP's error.Kuntz
Abion47 is right, I have other methods that call notifyListeners() twice without any problem. And in this case if I add a wait before first notify, then I can call notifyListeners() multiple times without any problem. But the finality of your answer is right, I could have simply assumed it's loading by default.Pug
does calling notifyListeners() inside markAsLoading method change anything? Looks the same to me.Romantic
@Romantic No, it doesn't change anything. Moving the second call to notifyListeners to within another function doesn't change the fact that both calls are happening within the scope of fetchEvents. Though like I said before, calling notifyListeners twice in quick succession like this, while often redundant, is relatively harmless on its own.Kuntz
Ok, I ready your answer more carefully, and I think you're right. Delegating UI logic to a Provider doesn't look like a good idea. It would be nice to read some practical examples on how to integrate a Provider for such goals. Anything to suggest? thanksRomantic
H
1

You are calling Apis from within your code for the root widget. Within Apis, you call notifyListeners, which, among other things, calls setState on widgets that are listening to the event provider. So firstly remove setState in your code and make sure Future use when call apis in init state

@override
  void initState() {
    super.initState();
    Future.microtask(() => context.read<SellCarProvider>().getBrandService(context));
  }
Halie answered 22/1, 2023 at 19:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.