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

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

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

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

Related

Flutter Cubit InitState

I am at the begin of my Cubit learning and i tried to create a "Liter-Tracker" with sharedPrefs. Everything works but not the init state. I have to press a Button first because I initialize the drinkValue with 0. I tried to return an Int with the value from shared prefs but this dont work :( May you help me?
This is my cubit:
class DrinkCubit extends Cubit<DrinkState> {
DrinkCubit() : super(DrinkState(drinkValue: 0));
Future<void> loadCounter() async {
final prefs = await SharedPreferences.getInstance();
state.drinkValue = (prefs.getInt('counter') ?? 0);
}
Future<int> loadInitCounter() async {
final prefs = await SharedPreferences.getInstance();
return state.drinkValue = (prefs.getInt('counter') ?? 0);
}
}
and this my cubit state:
class DrinkState {
int drinkValue;
int? amount;
DrinkState({
required this.drinkValue,
});
}
I also tried something like this in my MainPage, how i usually init my state with setState:
#override
void initState() {
super.initState();
BlocProvider.of<DrinkCubit>(context).loadCounter();
}
Context is not accessible in initstate, try using didChangeDependencies life cycle method Flutter get context in initState method
Firstly, I strongly advise you to avoid the StatefulWidget when you use BLoC, but it doesn't mean you can't use it at all. Just be careful because setState() can rebuild BlocProvider inside the stateful widget.
As for the initialization process, I suggest you use this approach on the BlocProvider.
class DrinkScreen extends StatelessWidget {
const DrinkScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => DrinkCubit()..loadCounter(), // Or call other initialization method
child: DrinkView(),
);
}
}
This approach works really well if you reuse this screen multiple times, for example, you redirect to DrinkScreen every time you want to fill data and you dispose of the screen afterward (Navigate.pop(), etc). This way you can automatically initialize the cubit every time you redirect into this screen, you don't need to use StatefulWidget to init the cubit.

Flutter : Why build widget runs first before the initState()?

This is my controller.dart file which checks if users is verified or not and then return the page according to the conditions.
My question is that why the build widget is executing first before initState() ? I tried to debug this code using breakpoints and noticed that build() widget is running first and then the initState()Why this is happening and how could I fix it ?
This is my code :
class _ControllerState extends State<Controller> {
late bool auth;
#override
Widget build(BuildContext context) {
return (auth==false) ? Onbording() : IndexPage();
}
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) async {
await this.checked_if_logged();
});
}
Future<void> checked_if_logged() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getBool('verified')==true){
setState(() {
auth = true;
});
}else{
setState(() {
auth = false;
});
}
}
}
This is a snapshot of my debug code where the blue line is showing that it runs first before init and because the bool auth is a late type so it throws lateInitializationErrror and after that initState() is called which initializes the auth variable which rebuild the widget and removes the error
Update:
I noticed that when I replace the WidgetsBinding.instance!.addPostFrameCallback((_) with just check_if_logged(), the initState() is calling first but before completion of check_if_logged() the build widget executes first which again throws lateInitializationError
I don't know where you got addPostFrameCallback from or what you want to achieve, but this is not the way.
Your problem is, that checked_if_logged is async and there is no way to await an async method in initState. That is by design and there is no way around that.
The proper way to handle this is to use a FutureBuilder widget.
See What is a Future and how do I use it?

Flutter Provider with listen false, but still get error "setState() or markNeedsBuild() called during build."

I would like to reset userProvider when networkProvider changes.
In the userProvider.reset(), I have notifyListeners.
void didChangeDependencies() async {
super.didChangeDependencies();
final NetworkProvider networkProvider = Provider.of<NetworkProvider>(context);
UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
userProvider.reset(); }
When it runs, it gives error "setState() or markNeedsBuild() called during build."
My question is, I have set the listen to false, why it still rebuild this widget?
When there is only a UserProvider, it has the same error:
void didChangeDependencies() async {
super.didChangeDependencies();
UserProvider userProvider = Provider.of<UserProvider>(context, listen: false);
userProvider.reset(); }
If my idea of usage is totally wrong, is there any suggestion to achieve the same result?
Because you mention that NetworkProvider changes, I'm guessing it's also publishing notifications in which case you'd also want this to not trigger rebuilds with the listen: false parameter. It seems like you do want those notifications though so that you can reset your UserProvider. I would suggest that you don't try and do this in a Widget's lifecycle but use a ProxyProvider to create and update your UserProvider with it being dependent on the NetworkProvider.
All right, I found the answer. It is writen in latter of their home page.
What I tried in my question is not allowed, because the state update is synchronous.
This means that some widgets may build before the mutation happens (getting an old value), while other widgets will build after the mutation is complete (getting a new value). This could cause inconsistencies in your UI and is therefore not allowed.
Instead, you should perform that mutation in a place that would affect the entire tree equally:
class MyNotifier with ChangeNotifier {
MyNotifier() {
_fetchSomething();
}
Future<void> _fetchSomething() async {
}
To use it in build:
Future.microtask(() =>
context.read<MyNotifier>().fetchSomething(someValue);
);

Flutter: Async function in Getx Controller takes no effect when initialized

Updates:
2021/06/11 After hours of debugging yesterday, I confirmed that the problem is caused by aws amplify configuration: _configureAmplify(). Because the location of the amplify server was set wrong, so _configureAmplify() takes several seconds to work... and therefore, the readPost() function did not work on initialization, as it must run after _configureAmplify()...
2021/06/10I made changes to my code according to S. M. JAHANGIR's advice, and updated the question. The issue still presists. The value of posts is not updated when called in initialization and the data only shows up after reload. (if I commented out the _controller.readPost() in UI, the value of posts is always empty.
I have this page that loads information from aws amplify with getx implemented. However, I found out the readPost() async funtion in getx controller dart file is not reading from database, when the controller instance is initialized. I have to add a _controller.readPost() in UI file to make it work. And the data only shows up after a reload of that UI page...
Getx Controller dart file:
class ReadPostController extends GetxController {
var isLoading = true.obs;
var posts = <Posty>[].obs;
#override
void onInit() {
_configureAmplify();
await readPost();
super.onInit();
// print('show post return value: $posts');
}
void _configureAmplify() {
final provider = ModelProvider();
final dataStorePlugin = AmplifyDataStore(modelProvider: provider);
AmplifyStorageS3 storage = new AmplifyStorageS3();
AmplifyAuthCognito auth = new AmplifyAuthCognito();
AmplifyAPI apiRest = AmplifyAPI();
// Amplify.addPlugin(dataStorePlugin);
Amplify..addPlugins([dataStorePlugin, storage, auth, apiRest]);
Amplify.configure(amplifyconfig);
print('Amplify configured');
}
// read all posts from databases
Future readPost() async {
try {
isLoading(true);
var result = await Amplify.DataStore.query(Posty.classType);
print('finish loading request');
result = result.sublist(1);
posts.assignAll(result);
// print(the value of posts is $posts');
} finally {
isLoading(false);
}
}
#override
void onClose() {
// called just before the Controller is deleted from memory
super.onClose();
}
}
And in the UI part:
class TabBody extends StatelessWidget {
TabBody({Key? key}) : super(key: key);
final ReadPostController _controller = Get.put(ReadPostController());
#override
Widget build(BuildContext context) {
_controller.readPost();//if commented out, _controller.post is empty
return Container(
child: Obx(
() => Text('showing:${_controller.posts[1].title}'),
));
}
}
In my understanding, the readPost() function should be called when the ReadPost_controller is initiallized. And the UI will update when the posts = <Posty>[].obs changes. Guys, what am I doing wrong here?
First, when you are calling readPost on onInit you are not awaiting. So change it to:
onInit() async{
...
await readPost();
...
}
Secondly, posts is a RxList so you need to use the assignAll method to update it.
Therefore, in your readPost method, instead of posts.value = reault you need to use posts.assignAll(result)
Calling from the UI works because readPost every time the build method is called by the Flutter framework and actually the UI shows the data from every previous call.
I think try with GetBuilder instead of Obx.
GetBuilder<ReadPostController>(
builder: (value) => Text('showing:${value.posts[1].title}'),
)
and also use update(). in readPost() method.

Flutter provider in initState

I'm currently trying Provider as a state management solution, and I understand that it can't be used inside the initState function.
All examples that I've seen call a method inside a derived ChangeNotifier class upon user action (user clicks a button, for example), but what if I need to call a method when initialising my state?
Motivation:
Creating a screen which loads assets (async) and shows progress
An example for the ChangeNotifier class (can't call add from initState):
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
}
You can call such methods from the constructor of your ChangeNotifier:
class MyNotifier with ChangeNotifier {
MyNotifier() {
someMethod();
}
void someMethod() {
// TODO: do something
}
}
Change your code to this
class ProgressData extends ChangeNotifier {
double _progress = 0;
double get progress => _progress;
void add(double dProgress) async {
// Loading Assets maybe async process with its network call, etc.
_progress += dProgress;
notifyListeners();
}
ProgressData() {
add();
}
}
In initState all the of(context) things don't work correctly, because the widget is not fully wired up with every thing in initState.
You can use this code:
Provider.of<ProgressData>(context, listen: false).add(progress)
Or this code:
Future.delayed(Duration.zero).then(_){
Provider.of<ProgressData>(context).add(progress)
}):
So an AssetLoader class which reports on its progress will look something like this, I guess:
import 'package:flutter/foundation.dart';
class ProgressData extends ChangeNotifier {
double _progress = 0;
ProgressData() {
_loadFake();
}
Future<void> _loadFake() async {
await _delayed(true, Duration(seconds: 1));
_add(1.0);
await _delayed(true, Duration(seconds: 2));
_add(2.0);
await _delayed(true, Duration(seconds: 3));
_add(3.0);
}
// progress
double get progress => _progress;
// add
void _add(double dProgress) {
_progress += dProgress;
notifyListeners();
}
// _delayed
Future<dynamic> _delayed(dynamic returnVal, Duration duration) {
return Future.delayed(duration, () => returnVal);
}
}
As Fateme said:
the widget is not fully wired up with everything in initState
Also, you can use something like this in your initState
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Provider.of<ProgressData>(context, listen: false).add(5);
});
I think it's more standard!
Be aware that you should use the correct context! I mean the context of the Builder!
The problem here lies with the fact that context does not exist yet in initState as extensively explained by the other answers. It doesn't exist because it hasn't yet been made a part of the widget tree.
Calling a method
If you're not assigning any state and only calling a method then initState would be the best place to get this done.
// The key here is the listen: false
Provider.of<MyProvider>(context, listen: false).mymethod();
The code above is allowed by Flutter because it doesn't have to listen for anything. In short, it's a one off. Use it where you only want to do something instead of read/listen to something.
Listening to changes
Alternatively, if you need to listen to changes from Provider then the use of didChangeDependencies would be the best place to do so as context would exist here as in the docs.
This method is also called immediately after initState.
int? myState;
#override
void didChangeDependencies() {
// No listen: false
myState = Provider.of<MyProvider>(context).data;
super.didChangeDependencies();
}
If you've never used didChangeDependencies before, what it does is get called whenever updateShouldNotify() returns true. This in turn lets any widgets that requested an inherited widget in build() respond as needed.
I'd usually use this method in a FutureBuilder to prevent reloading data when data already exists in Provider after switching screens. This way I can just check Provider for myState and skip the preloader (if any) entirely.
Hope this helps.