I'm trying to preserve the state of a widget, so that if I temporarily remove the stateful widget from the widget tree, and then re-add it later on, the widget will have the same state as it did before I removed it. Here's a simplified example I have:
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool showCounterWidget = true;
@override
Widget build(BuildContext context) {
return Material(
child: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
showCounterWidget ? CounterButton(): Text("Other widget"),
SizedBox(height: 16,),
FlatButton(
child: Text("Toggle Widget"),
onPressed: (){
setState(() {
showCounterWidget = !showCounterWidget;
});
},
)
],
),
),
);
}
}
class CounterButton extends StatefulWidget {
@override
_CounterButtonState createState() => _CounterButtonState();
}
class _CounterButtonState extends State<CounterButton> {
int counter = 0;
@override
Widget build(BuildContext context) {
return MaterialButton(
color: Colors.orangeAccent,
child: Text(counter.toString()),
onPressed: () {
setState(() {
counter++;
});
},
);
}
}
Ideally, I would not want the state to reset, therefor the counter would not reset to 0, how would I preserve the state of my counter widget?
The reason why the widget loose its state when removed from the tree temporarily is, as Joshua stated, because it loose its Element/State.
Now you may ask:
Can't I cache the Element/State so that next time the widget is inserted, it reuse the previous one instead of creating them anew?
This is a valid idea, but no. You can't. Flutter judges that as anti-pattern and will throw an exception in that situation.
What you should instead do is to keep the widget inside the widget tree, in a disabled state.
To achieve such thing, you can use widgets like:
These widgets will allow you to keep a widget inside the widget tree (so that it keeps its state), but disable its rendering/animations/semantics.
As such, instead of:
Widget build(context) {
if (condition)
return Foo();
else
return Bar();
}
which would make Foo
/Bar
loose their state when switching between them
do:
IndexedStack(
index: condition ? 0 : 1, // switch between Foo and Bar based on condition
children: [
Foo(),
Bar(),
],
)
Using this code, then Foo
/Bar
will not loose their state when doing a back and forth between them.
Widgets are meant to store transient data of their own within their scope and lifetime.
Based on what you have provided, you are trying to re-create CounterButton
child widget, by removing and adding it back to the widget tree.
In this case, the counter value that is under the CounterButton
was not saved or not saving in the MyHomePage
screen, the parent widget, without any reference to a view model or any state management within or at the top level.
A more technical overview how Flutter renders your widgets
Ever wonder what is the key
if you try to create a constructor for a widget?
class CounterButton extends StatefulWidget {
const CounterButton({Key key}) : super(key: key);
@override
_CounterButtonState createState() => _CounterButtonState();
}
keys (key
) are identifiers that are automatically being handled and used by the Flutter framework to differentiate the instances of widgets in the widget tree. Removing and adding the widget (CounterButton
) in the widget tree resets the key
assigned to it, therefore the data it holds, its state are also removed.
NOTE: No need to create constructors for the a
Widget
if it will only containkey
as its parameter.
From the documentation:
Generally, a widget that is the only child of another widget does not need an explicit key.
Why does Flutter changes the key assigned to the CounterButton?
You are switching between CounterButton
which is a StatefulWidget
, and Text
which is a StatelessWidget
, reason why Flutter identifies the two objects completely different from each other.
You can always use Dart Devtools to inspect changes and toggle the behavior of your Flutter App.
Keep an eye on #3a4d2
at the end of the _CounterButtonState
.
This is the widget tree structure after you have toggled the widgets. From CounterButton
to the Text
widget.
You can now see that the CounterButton
ending with #31a53
, different from the previous identifier because the two widgets are completely different.
What can you do?
I suggest that you save the data changed during runtime in the _MyHomePageState
, and create a constructor in CounterButton
with a callback function to update the values in the calling widget.
counter_button.dart
class CounterButton extends StatefulWidget {
final counterValue;
final VoidCallback onCountButtonPressed;
const CounterButton({Key key, this.counterValue, this.onCountButtonPressed})
: super(key: key);
@override
_CounterButtonState createState() => _CounterButtonState();
}
class _CounterButtonState extends State<CounterButton> {
@override
Widget build(BuildContext context) {
return MaterialButton(
color: Colors.orangeAccent,
child: Text(widget.counterValue.toString()),
onPressed: () => widget.onCountButtonPressed(),
);
}
}
Assuming you named your variable _counterValue
in the _MyHomePageState
, you can use it like this:
home_page.dart
_showCounterWidget
? CounterButton(
counterValue: _counterValue,
onCountButtonPressed: () {
setState(() {
_counterValue++;
});
})
: Text("Other widget"),
In addition, this solution will help you re-use CounterButton
or other similar widgets in other parts of your app.
I've added the complete example in dartpad.dev.
Andrew and Matt gave a great talk how Flutter renders widgets under the hood:
Further reading
The real solution to this problem is state management. There are several good solutions for this available as concepts and flutter packages. Personally I use the BLoC pattern regularly.
The reason for this is that widget state is meant to be used for UI state, not application state. UI state is mostly animations, text entry, or other state that does not persist.
The example in the question is application state as it is intended to persist longer than the live time of the widget.
There is a little Tutorial on creating a BLoC based counter which could be a good starting point.
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