If you have been developing apps on Flutter long enough, chances are you've been making use of a state management tool, like Provider, BLoC, MobX, or the like.
If so, there might be a chance you've stumbled upon a certain problem, whereby a StatefulWidget
doesn't choose to update itself upon state change.
This is a pretty specific problem. So in this article, we will walk through the problem and its solution via examples.
If you just want to know the summary, feel free to scroll downwards to the Summary section.
Building the Counter App
Let's start off with the Hello World of Flutter app, the Counter App. For this, we will be using MobX as its state management tool.
counter_store.dart
import 'package:mobx/mobx.dart';
part 'counter_store.g.dart';
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store {
@observable
int sum = 0;
@action
void increment() {
sum++;
}
}
Whether you are familiar with MobX or not, just understand that we have a store that contains a data member of sum
, and a method that adds said data member by 1.
my_home_page.dart
class MyHomePage extends StatelessWidget {
final counterStore = CounterStore();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter'),
),
body: Center(
child: Observer(
builder: (_) => StatelessCounterBody(
value: counterStore.sum,
onPressedAddButton: counterStore.increment,
),
),
),
);
}
}
stateless_counter_body.dart
class StatelessCounterBody extends StatelessWidget {
final int value;
final GestureTapCallback onPressedAddButton;
const StatelessCounterBody({
Key? key,
required this.value,
required this.onPressedAddButton,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$value',
style: Theme.of(context).textTheme.headline4,
),
const Padding(padding: EdgeInsets.all(16.0)),
FloatingActionButton(
onPressed: onPressedAddButton,
tooltip: 'Increment',
child: Icon(Icons.add),
),
],
);
}
}
This Counter App here is done a bit differently. We've refactored the content as a StatelessWidget
with the name of StatelessCounterBody
.
From where this StatelessCounterBody
is used (my_home_page.dart), once the onPressedAddButton
callback is triggered, counterStore.increment()
is called to set the store's value
. Therefore the wrapping Observer
widget will discard the current instance and rebuild a new instance of StatelessCounterBody
, where the value of its value
parameter is the new value of sum
.
Sweet! Works well so far.
Going Reactive, Going Stateful
Now comes a thought, of making a StatefulWidget
version of StatelessCounterBody
, and we want to call it as StatefulCounterBody
.
Why would we want to do that? That's because now we want to make this widget reactive than static.
And yes, setState
shall be used for internal rebuilding upon state changes for this widget in mind, because we do not want it to be reliant on other external means to have its state mutated (for example, expecting a certain store of a certain state management tool as its parameter and mutations are done with it).
stateful_counter_body.dart
class StatefulCounterBody extends StatefulWidget {
final int initialValue;
final Function(int) onValueIncrement;
const StatefulCounterBody({
Key? key,
required this.initialValue,
required this.onValueIncrement,
}) : super(key: key);
@override
_StatefulCounterBodyState createState() => _StatefulCounterBodyState();
}
class _StatefulCounterBodyState extends State<StatefulCounterBody> {
late int _value;
@override
void initState() {
super.initState();
_value = widget.initialValue;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_value',
style: Theme.of(context).textTheme.headline4,
),
const Padding(padding: EdgeInsets.all(16.0)),
FloatingActionButton(
onPressed: () {
setState(() {
_value++;
widget.onValueIncrement(_value);
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
],
);
}
}
For the case of StatefulCounterBody
, we want it to react to the value changes all in itself. From the outside, the initialValue
is to be provided, as well as a callback called onValueIncrement
, where in itself returns the updated value upon incrementing.
Changed My Mind
So now, we decided that we want to add another button at the home page, where upon click adds the sum
by 5.
counter_store.dart
import 'package:mobx/mobx.dart';
part 'counter_store.g.dart';
class CounterStore = _CounterStore with _$CounterStore;
abstract class _CounterStore with Store {
@observable
int sum = 0;
@action
void increment() {
sum++;
}
// Newly added!
@action
void incrementByFive() {
sum += 5;
}
}
Hence, adding incrementByFive()
to the store.
Making Use Of That StatefulWidget
my_home_page.dart
class MyHomePage extends StatelessWidget {
final counterStore = CounterStore();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter'),
),
body: Center(
child: Observer(
builder: (_) => StatefulCounterBody(
initialValue: counterStore.sum,
onValueIncrement: (value) {
counterStore.sum = value;
},
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: counterStore.incrementByFive,
tooltip: 'Add by 5',
child: Icon(Icons.add),
),
);
}
}
Button added, StatefulCounterBody
ready. Let's give it a go.
Nice! Now let's try the newly added button; bottom right.
Wait. Why isn't that working?!
The Fact about StatefulWidgets
Diagram by Flutter Clutter. Check out his article about StatelessWidget vs. StatefulWidget
Whenever a StatefulWidget
is constructed, the State
first gets created via createState()
, then the StatefulWidget
is built. As the state changes, while the StatefulWidget
gets discarded and rebuilt, (internally via setState()
or externally via Observer
, BlocBuilder
, etc.), the State
remains.
For our case, as counterStore.incrementByFive()
was called and the Observer
tries to externally rebuild a new instance of StatefulCounterBody
, the instance was rebuilt, but the State
persisted.
That is because we are yet to add something that informs the StatefulWidget
to look out for the differences between the old and the new instances that should affect the state.
Art Thou Updated?
@override
void didUpdateWidget(covariant StatefulCounterBody oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialValue != widget.initialValue) {
_value = widget.initialValue;
}
}
Therefore, the lifecycle method we can make use of is didUpdateWidget()
.
If there's a difference between the previous instance and the new instance, then it should update the state with that of the new instance accordingly.
Brilliant! It now works! :D
Summary
If you have problems trying to externally rebuild a StatefulWidget
with Observer
, BlocBuilder
or by however rebuilding is done in other state management tools, then the widget has to know how to be aware of updating its State
accordingly if there's found to be differences between the old and the new instance by using the didUpdateWidget()
lifecycle method.
Top comments (0)