Flutter reset StatefulWidget state when Widget is recreated - flutter

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

Related

Flutter Difference between InitState and just putting inside Widget Build function

I had an error every time I restarted my App: This widget has been unmounted, so the State no longer has a context (and should be considered defunct). and saw that something was not correct with my initstate. The initState was:
#override
void initState() {
SchedulerBinding.instance.addPostFrameCallback((_) {
BlocProvider.of<TutSkippedCubit>(context).load();
});
super.initState();
}
the methods loads the data from sharedprefs if I have already skipped the tut or not. Now I solved this issue with removing the initState method and putting the function call inside the widget build:
#override
Widget build(BuildContext context) {
BlocProvider.of<TutSkippedCubit>(context).load();
....
The widget build gets called when the pages loads, so, isn't it the same as the initial state? For what exactly is the methode initState() and I have the feeling that my way of handling this problem is a bad practise, but what would be a better way, how do I solve it?
The initState() method is to control what happens after the app is built. The problem is that you call BlocProvider before the app begins. The correct way is to put all the actions after super.initState() call and add the context to the BlocProvider inside build method. Like this:
TutSkippedCubit? tutSkippedCubitProvider;
#override
void initState() {
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) {
tutSkippedCubitProvider!.load();
});
}
#override
Widget build(BuildContext context) {
tutSkippedCubitProvider = BlocProvider.of<TutSkippedCubit>(context);
...
}
The initState and build method is called when the widget is inserted into the widget tree, but the build method also is called every time the state is changed.
You do need to have in mind that every time the state is changed your method BlocProvider.of<TutSkippedCubit>(context).load(); also is called.
Maybe, the code below can help you:
WidgetsBinding.instance.endOfFrame.then(
(_) async {
if (mounted) {
BlocProvider.of<TutSkippedCubit>(context).load();
}
},
);
You wouldn't be surprise of getting that error since you are using BlocProvider.<T>(context) out of a BuildContext. This context in bracket is the just the same as the one given in the build function.
The initState() is a method that is called when an object for your
stateful widget is created and inserted inside the widget tree.

Post Frame Callback - Looping

I have been checking performance on my app and noticed that one of the widget constantly loops. in that Widget I am using the following code to retrieve data from Firestore DB, however for this example I have simplified it with the same looping result.
Question: Is there a reason why Widget Binding is called so many times and in a loop? I was under the impression it was called once on widget build complete. Should I be using something else for a one off post build function?
I have 7 of these widgets in a listView, so I should she maximum 7 I believe.
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) async {
print(' Is callback done?');
if (mounted) {
setState(() {
isLoaded = true;
});
}
});
}
The widget itself is very simple with just a Text Widget
#override
Widget build(BuildContext context) {
return Text('Hello');
);
}
In my logs I see the following which just keeps going up and up and up.
Maybe you want addPostFrameCallback instead of addPersistentFrameCallback ?
As per documentation addPersistentFrameCallback:
Once registered, they are called for every frame for the lifetime of the application.
As per documentation addPostFrameCallback:
Post-frame callbacks cannot be unregistered. They are called exactly once.

How to persist value from a Future when switching between pages in Flutter?

I am trying to add a widget to the screen when the future has data in Screen 1.
class UserChoiceBooks extends StatelessWidget {
final String title;
UserChoiceBooks({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: Provider.of<Books>(context, listen: false)
.getRecommendedBooks("test"),
builder: (ctx, snapshot) {
// Checking if future is resolved
if (snapshot.connectionState == ConnectionState.done) {
// If we got an error
if (snapshot.hasError) {
return Center(
child: Text(
'${snapshot.error} occured',
style: TextStyle(fontSize: 18),
),
);
// if we got our data
} else if (snapshot.hasData) {
// Extracting data from snapshot object
final List<Book> recommendedBooksML = snapshot.data;
return BookList(title, recommendedBooksML);
} else {
return SizedBox.shrink();
}
} else {
return Container();
}
}
// ... some code here
);
}
}
I navigate to a different screen Screen 2.
And then back to Screen 1. I loose the future data and it starts Building again.
PS:I found a temporary workaround by storing the Future data in a Global Variable
List<Book> placeholder=[];
And then setting the value from future...placeholder=snapshot.data;
When Switching between Screen 1 and 2. I am checking
placeholder==[]?UserChoiceBooks (title):BookList(title,placeholder)
Is there a better way to keep data from snapshot so that switching between screen future value is not Lost?
You are making a call during your build:
Provider.of<Books>(context, listen: false).getRecommendedBooks("test")
Thus when the screen is rebuilt, it is invoked again. You are only too lucky that no other rebuilds are happening between those two navigations, because in theory they are possible.
A recommended mindset is that a rebuild may happen at each frame, and non-rebuilding frames should be treated as a framework optimization.
So you should make a call one time and save it. There are a few options:
Create Books somewhere up the tree and pass it to the widget. Then make your widget stateful and call widget.books.getRecommendedBooks in its initState().
If Books is a singleton, you may use a service locator like https://pub.dev/packages/get_it to instantiate it once and then use GetIt.instance.get to fetch this instance anywhere in your widget. A service locator pattern has some advantages over the provider pattern.
A nasty way is to call the future in a stateful widget's state and call the method on first build if _future == null.
For stateful widgets see this tutorial: https://flutter.dev/docs/development/ui/interactive#stateful-and-stateless-widgets
Also note that you should use a key for such a widget that would be different for different titles that you pass to the constructor. Otherwise, when you replace your widget with one with another title, the framework would not know that all that follows is for another book and will not create a new state, thus initState() will not be called.

Flutter firebase provider architecture

I have a stateful widget that has an animated container. Inside that animated container I have a streamProvider connected to firebase. My problem is that when I animate using setState the entire widget rebuilds and another call to firebase is made. My solution was to lift the streamProvider up and wrap the widget that's animated with that streambuilder. But that means I need to create another widget and hence more boilerplate.
I feel like what I'm doing is wrong but I'm kind of stuck because all provider resources are related to authentication...
Does anyone have any ideas how I can get around this in a clean way? and is setState the right way to trigger animations in a stateful widget?
For animating, try using AnimatedBuilder its the easiest way to animate, but I guess it won't fix your issue.
Personally I always use the Provider package, I don't know if you are doing it too.
So usually firebase provides you with a stream of data (if you are using it with cloud functions its different)
Now you could use a StreamBuilder with the Stream firebase provides you and use the data of the stream. With this version rebuilding the Widget won't lead to the app connecting to the server and fetching new data.
If you really like to use a ChangeNotifier you can use that stream inside the ChangeNotifier, listen to it and always notifying listeners of changes to occur with this implementation there won't be any unnecessary network calls either.
Some examples for the second version:
class SomeNotifier extends ChangeNotifier {
List<MyData> dataList = [];
SomeNotifier() {
Firestore.instance.collection("MyCollection").snapshots().listen((data) {
dataList = data.documents.map((doc) => MyData.fromDoc(doc));
notifyListeners();
});
}
}
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
AnimationController _controller;
#override
void initState() {
_controller = AnimationController(vsync: this);
super.initState();
}
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<SomeNotifier>(
create: (context) => SomeNotifier(),
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
var notifier = Provider.of<SomeNotifier>(context);
return Container(); //Here you can use your animated widget, it will be rebuilt to animate propperly
//It will also rebuild every time data in firebase changes
},
),
);
}
}
I hope this answers your question.

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

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.