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?
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;
...
Future<void> fetchEvents(String email) async {
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With