Flutter exception `setState() or markNeedsBuild() called during build` by calling setState in a NotificationListener callback - flutter

I'm having an hard time trying to figure out how to update a piece of a view based on a child view "event"... let me explain:
I have a screen which is composed by a Scaffold, having as a body a custom widget which calls a rest api to get the data to display (it makes use of a FutureBuilder), then it dispatch a Notification (which basically wraps the Flutter's AsynchSnapshot) that should be used in order to update the floatingActionButton (at least in my mind :P).
This is the build method of the screen:
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: onMyNotification,
child: Scaffold(
appBar: MyAppBar(),
body: MyRemoteObjectView(),
floatingActionButton: MyFloatingButton(),
),
);
}
The view is rendered perfectly, the data is retrieved from the server and displayed by MyRemoteObjectView and the notification is successfully dispatched and received, BUT as soon as I call setState() in my callback, I get the exception:
setState() or markNeedsBuild() called during build.
This is the callback (defined in the same class of the build method above):
bool onMyNotification(MyNotification notification) {
AsyncSnapshot snapshot = notification.snapshot;
if (snapshot.connectionState == ConnectionState.done) {
setState(() {
// these flags are used to customize the appearance and behavior of the floating button
_serverHasBeenCalled = true;
_modelDataRetrieved = snapshot.hasData;
});
}
return true;
}
This is the point in which I send the notification (build method of MyRemoteObjectView's state):
#override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
MyNotification(snapshot).dispatch(context);
// ...
The point is: how and when should I tell Flutter to redraw the floating button (and/or other widgets)? (because of course without the setState I don't have the exception but the button is not refreshed)
Am I getting the whole thing wrong? Is there an easier way to do it? Let me know

After FutureBuilder is built, it waits for future to return a value. After it is complete, you're calling setState and then FutureBuilder would be built again and so on, resulting in infinite repaint loop.
Are you sure that you need FutureBuilder and NotificationListener in this case? You should probably do it in initState of your StatefulWidget like this:
#override
void initState() {
super.initState();
getData().then((data) {
setState(() {
_serverHasBeenCalled = true;
_modelDataRetrieved = true;
});
});
}
You can also store Future in a state and pass it to FutureBuilder.

Related

Flutter - how to correctly create a button widget that has a spinner inside of it to isolate the setState call?

I have a reuable stateful widget that returns a button layout. The button text changes to a loading spinner when the network call is in progress and back to text when network request is completed.
I can pass a parameter showSpinner from outside the widget, but that requires to call setState outside of the widget, what leads to rebuilding of other widgets.
So I need to call setState from inside the button widget.
I am also passing a callback as a parameter into the button widget. Is there any way to isolate the spinner change state setting to inside of such a widget, so that it still is reusable?
The simplest and most concise solution does not require an additional library. Just use a ValueNotifier and a ValueListenableBuilder. This will also allow you to make the reusable button widget stateless and only rebuild the button's child (loading indicator/text).
In the buttons' parent instantiate the isLoading ValueNotifier and pass to your button widget's constructor.
final isLoading = ValueNotifier(false);
Then in your button widget, use a ValueListenableBuilder.
// disable the button while waiting for the network request
onPressed: isLoading.value
? null
: () async {
// updating the state is super easy!!
isLoading.value = true;
// TODO: make network request here
isLoading.value = false;
},
#override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: isLoading,
builder: (context, value, child) {
if (value) {
return CircularProgressIndicator();
} else {
return Text('Load Data');
}
},
);
}
You can use StreamBuilder to solve this problem.
First, we need to create a stream. Create a new file to store it, we'll name it banana_stream.dart, for example ;).
class BananaStream{
final _streamController = StreamController<bool>();
Stream<bool> get stream => _streamController.stream;
void dispose(){
_streamController.close();
}
void add(bool isLoading){
_streamController.sink.add(isLoading);
}
}
To access this, you should use Provider, so add a Provider as parent of the Widget that contain your reusable button.
Provider<BananaStream>(
create: (context) => BananaStream(),
dispose: (context, bloc) => bloc.dispose(),
child: YourWidget(),
),
Then add the StreamBuilder to your button widget:
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: Provider.of<BananaStream>(context, listen:false),
initialData: false,
builder: (context, snapshot){
final isLoading = snapshot.data;
if(isLoading == false){
return YourButtonWithNoSpinner();
} else{
return YourButtonWithSpinner();
}
}
);
}
}
And to change isLoading outside, you can use this code:
final provider = Provider.of<BananaStream>(context, listen:false);
provider.add(true); //here is where you change the isLoading value
That's it!
Alternatively, you can use ValueNotifier or ChangeNotifier but i find it hard to implement.
I found the perfect solution for this and it is using the bloc pattern. With this package https://pub.dev/packages/flutter_bloc
The idea is that you create a BLOC or a CUBIT class. Cubit is just a simplified version of BLOC. (BLOC = business logic component).
Then you use the bloc class with BlocBuilder that streams out a Widget depending on what input you pass into it. And that leads to rebuilding only the needed button widget and not the all tree.
simplified examples in the flutter counter app:
// input is done like this
onPressed: () {
context.read<CounterCubit>().decrement();
}
// the widget that builds a widget depending on input
_counterTextBuilder() {
return BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
if (state.counterValue < 0){
return Text("negative value!",);
} else if (state.counterValue < 5){
return Text("OK: ${state.counterValue}",
);
} else {
return ElevatedButton(onPressed: (){}, child: const Text("RESET NOW!!!"));
}
},
);
}

how to call setState inside FutureBuilder in Flutter

I'm trying to set State based on result received by FutureBuilder, looks like it is not possible since FutureBuild is still building, any ideas please ? error :
The following assertion was thrown building FutureBuilder<String>(dirty, state: _FutureBuilderState<String>#db9f1):
setState() or markNeedsBuild() called during build.
This StatefulBuilder widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: StatefulBuilder
my code :
FutureBuilder<String>(
future: fetchIdPlayer(idPlayer, bouquet),
builder: (context, snapid) {
if (!snapid.hasData)
return Container(
height: mobileHeight * 0.05,
child: Center(
child: CircularProgressIndicator(),
),
);
else if (snapid.data == "Error_ID") {
setState(() {
have_ID = true;
resultName = "رقم تعريف اللاعب خاطئ";
});
}
})
You can workaround the error you are getting by scheduling the setState to be executed in the next frame and not potentially during build.
else if (snapid.data == "Error_ID") {
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
///This schedules the callback to be executed in the next frame
/// thus avoiding calling setState during build
setState(() {
have_ID = true;
resultName = "رقم تعريف اللاعب خاطئ";
});
});
...
You can just wrap the widget who will use resultName and have_ID with FutureBuilder .So there is no need to setState.you can also handle error as well .If you want to setState then use a asyn function and after result is fetched you can just call setState
You could use the property connectionState of snapid.
This should generally work as connectionState is set to ConnectionState.done whenever the future is terminated.
FutureBuilder<String>(
future: fetchIdPlayer(idPlayer, bouquet),
builder: (context, snapid) {
if (snapshot.connectionState == ConnectionState.done) {
setState((){
//...
})
}
if (!snapid.hasData)
//...
else if (snapid.data == "Error_ID") {
//...
}
})
The key bit of this error message is, "called during build". You need to wait until after build. This can be accomplished with either:
WidgetsBinding.instance?.addPostFrameCallback()
https://api.flutter.dev/flutter/widgets/WidgetsBinding-mixin.html
or
SchedulerBinding.instance?.addPostFrameCallback()
https://api.flutter.dev/flutter/scheduler/SchedulerBinding-mixin.html
Flutter prohibit this kind of situation because setting a state in side a future widget will cause to rebuild the future widget again and this will become an infinite loop. Try to separate logic from UI. Example credits goes to #pskink and #Mofidul Islam
In your state class create a wrapper
Future<String> wrapper(idPlayer, bouquet) async {
final foo = await fetchIdPlayer(idPlayer, bouquet);
//handle errors
if(foo.error){
have_ID = true;
resultName = "رقم تعريف اللاعب خاطئ";
}
return foo;
}
call the wrapper in your widget
FutureBuilder<String>(
future: wrapper(idPlayer, bouquet),
builder: (context, snapid) {
if (!snapid.hasData)
return Container(
height: mobileHeight * 0.05,
child: Center(
child: CircularProgressIndicator(),
),
);
else {
//UI to show error
}
})

setState vs StreamProvider

I'm a Flutter newbie and I have a basic understanding question:
I built a google_maps widget where I get the location of the user and also would like to show the current location in a TextWidget.
So I'm calling a function in initState querying the stream of the geolocator package:
class _SimpleMapState extends State<SimpleMap> {
Position userPosStreamOutput;
void initPos() async {
userPosStream = geoService.getStreamLocation();
userPosStream.listen((event) {
print('event:$event');
setState(() {
userPosStreamOutput = event;
});
});
setState(() {});
}
#override
void initState() {
initPos();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold( //(very simplified)
body: Text(userPosStreamOutput.toString()),
This works just fine. But would it make sense to use a Streamprovider instead? Why?
Thanks
Joerg
An advantage to use StreamProvider is to optimize the process of re-rendering. As setState is called to update userPosStreamOutput value, the build method will be called each time your stream yields an event.
With a StreamProvider, you can apply the same logic of initializing a stream but then, you will use a Consumer which will listen to new events incoming and provide updated data to children widgets without triggering other build method calls.
Using this approach, the build method will be called once and it also makes code more readable in my opinion.
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamProvider(
create: (_) => geoService.getStreamLocation(),
initialData: null,
child: Consumer<Position>(
builder: (context, userPosStreamOutput, _) {
return Text(userPosStreamOutput.toString());
},
),
),
);
}

Flutter reuse state from StatefulWidget on rebuild

In short, the question is: How do I reuse the state of an entire widget subtree?
This is what my code currently looks like:
...
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is Authenticating) {
return AppLoading();
} else if (state is NotAuthenticated) {
return AppOnboarding();
} else if (state is Authenticated) {
return AppMain();
} else {
return null;
}
}
),
...
Nothing fancy here, just a BlocBuilder rebuilding its child whenever the state of the underlying Bloc changes.
Now, take for instance the following state transitions: NotAuthenticated => Authenticating => NotAuthenticated because there was something wrong with the inputed information. This would result in the AppOnboarding() widget being rebuild completely from scratch with all the information lost. How can I reuse the state from the old AppOnboarding() widget to make it look like the widget was never rebuild?
What I've already tried
I've already tried using a GlobalKey for this which I would pass to the key property of my AppOnboarding widget. This is my code:
_AuthenticatingState extends State<Authenticating> {
GlobalKey<AppOnboardingState> _appOnboardingKey;
#override
initState() {
super.initState();
_appOnboardingKey = GlobalKey();
}
#override
Widget build(BuildContext context) {
return BlocBuilder<...>(
builder: (context, state) {
...
if (state is NotAuthenticated) {
return AppOnboarding(key: _appOnboardingKey);
}
...
}
),
}
}
I was a little surprised this didn't work. Do global keys not maintain the state of the entrie widget-subtree?
Flutter runs at 60 frames per second. The key tells Flutter that some widget is the same one that existed in the last frame. If you remove some widget, even for a single frame, it will dispose the widget (call the dispose method and get rid of the widget).
In other words, the key doesn't "save" any state for later.
You have two options:
Don't dispose the widget at all.
builder: (context, state) {
return Stack(children: [
Offstage(child:AppLoading(), offstage: state is! Authenticating),
Offstage(child:AppOnboarding()), offstage: state is! NotAuthenticated),
Offstage(child:AppMain()), offstage: state is! Authenticated),
]) }
}
Save yourself that widget state, so that you can rebuild the widget later with that same information.

Flutter reset StatefulWidget state when Widget is recreated

In my Flutter Widget I have a StreamBuilder that checks for snapshot.hasError and in this specific case it will return my ErrorRetryWidget().
builder: (context, AsyncSnapshot<MyObject> snapshot) {
...
if (snapshot.hasError) {
return ErrorRetryWidget();
}
}
The ErrorRetryWidget() just shows an error message and a retry button. When you press this button, I replace the button text by a progress indicator. Therefore I needed to make this widget stateful, as its state holds a isRetrying variable that I set to false in the initState, and then to true once pressed.
When pressing the button, the ErrorRetryWidget tells the parent through a VoidCallback to re-trigger the stream logic. It works well but the issue is that if the error comes back, my StreamBuilder will "return" the ErrorRetryWidget once again.
The constructor is called a new time, but not initState. How can I make it so the state resets every time the widget is re-created? Therefore, isRetrying is already (or still) set to true.
The only quick solution I found was to implement this in my error widget:
#override
void didUpdateWidget(covariant oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() {
retrying = false;
});
}
Not sure it's a good practice.
Pass a unique key to let it create a new widget.
if (snapshot.hasError) {
return ErrorRetryWidget(key: UniqueKey());
}
I use didUpdateWidget just as you said, to reset the state of a stateful widget. It is also useful in animations.
The only comment I would add is to NOT use setState as you did because when the framework calls didUpdateWidget, it immediately calls build. Therefore, you don't have to trigger a call to build within didUpdateWidget. It ends up calling build two times.
#override
void didUpdateWidget(covariant oldWidget) {
super.didUpdateWidget(oldWidget);
retrying = false;
}