Matija Novosel
Posted on September 15, 2022
Introduction
One of the most important things to consider when making a Flutter application (or an application of the same sort) is how to manage state, that is to say data that changes in accordance to specified events or all purpose data that is shared across the entire app.
Some of these methods will be described and they are as follows:
- StatefulWidget
- StatefulBuilder
- InheritedWidget
- RxDart BehaviourSubject
- BLoC
- Redux
- Mobx
- Scoped Model
- Flutter Hooks
- Provider
- RiverPod
- Firebase
To demonstrate, a series of examples will be shown with a rudimentary use case - an app that tracks an incrementing counter.
StatefulWidget
StatefulWidget
is the most simple and common way of organizing state, however it is problematic because it needs a significant amount of boilerplate: two classes and a frequent call to the setState
function. That and it does not scale well when the application grows.
class Counter extends StatefulWidget {
const Counter({Key? key}) : super(key: key);
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _value = 1;
void increment() {
setState(() {
_value++;
});
}
@override
Widget build(BuildContext context) {
return Text("$_value");
}
}
StatefulBuilder
A simpler version of the StatefulWidget
, so to say. Logic is not separated into a new class and all of the state is located inside the StatefulBuilder
object.
Not ideal for scaling, but it is useful for building smaller parts of local state.
class Counter extends StatelessWidget {
int _value = 1;
Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StatefulBuilder(
builder: (context, StateSetter setState) => Column(
children: [
Text("$_value"),
TextButton(
onPressed: () => setState(() => _value++),
child: Text("Press me"),
),
],
),
);
}
}
InheritedWidget
Solves the problem of component communication if they are deeply nested within the component tree. There are alternatives and it is not ideal for common use, however it is implemented within a lot of built-in Flutter components.
It allows the sharing of state with child components, regardless of their depth.
class InheritedCounter extends InheritedWidget {
final Map _counter = {"val": 0};
final Widget child;
increment() {
_counter["val"]++;
}
get counter => _counter["val"];
InheritedCounter({
required this.child,
Key? key,
}) : super(
child: child,
key: key,
);
@override
bool updateShouldNotify(InheritedCounter oldWidget) => true;
static InheritedCounter? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<InheritedCounter>();
}
}
class Counter extends StatelessWidget {
const Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
int counter = InheritedCounter.of(context)?.counter;
Function? increment = InheritedCounter.of(context)?.increment;
return Column(children: [
Text("$counter"),
TextButton(
onPressed: () => setState(() => increment!()),
child: Text("Press me"),
),
]);
},
);
}
}
An object is declared with its own state extending the InheritedWidget
class, then it is exposed to the global scope of the application with the static of
method.
In the same object a function that changes the state is provided, along with the reactive state. Both can be accessed in a component the user chooses.
RxDart BehaviourSubject
This approach uses an easily understandable flow of data and it allows for easier testing and isolation of content.
In addition, it tightly integrates into the existing Flutter streams and builders and can be used as a replacement for the BLoC pattern.
class CounterService {
final BehaviourSubject _counter = BehaviourSubject.seeded(1);
Observable get stream$ => _counter.stream;
int get current => _counter.value;
increment() {
_counter.add(current + 1);
}
}
CounterService counterService = CounterService();
class Countner extends StatelessWidget {
Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: counterService.stream$,
builder: (BuildContext context, AsyncSnapshot snap) {
return Column(
children: [
Text(snap.data),
TextButton(
onPressed: () => counterService.increment(),
child: Text("Press me"),
),
],
);
},
);
}
}
The important features are encapsulated inside a class that possesses the BehaviourSubject
object. Using this object the user can monitor how the data changes and change the data itself. To access the data externally (inside a component) getters should be provided.
Acquiring the data afterwards is a breeze as the only thing the user must do is create an instance of the service then use the getters or the methods that mutate data.
The package is provided here.
BLoC
Short for Business Logic Componnent, it allows for the use of a one way data flow with the dedicated Cubit
class. It is very reusable and testable, changes in the state can be monitored very easily but it requires a bit more boilerplate.
Everything revolves around the wrapper component called BlocProvider
and instances of the Cubit
class as it is the fundamental building block of state.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
class Counter extends StatelessWidget {
Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterCubit(),
child: BlocBuilder<CounterCubit, int>(
builder: (BuildContext context, int count) {
return Column(
children: [
Text("$count"),
TextButton(
onPressed: () => {
context.read<CounterCubit>().increment();
},
child: Text("Press me"),
),
],
);
},
),
);
}
}
The package is provided here.
Redux
The de facto package for managing state all across popular Javascript frameworks makes its way into Flutter. To use the package some component wrapping is required, but in essence it is similar to BloC.
enum Actions { increment }
int counterReducer(int state, dynamic action) {
if (action == Actions.increment) {
return state + 1;
}
return state;
}
class Counter extends StatelessWidget {
final Store<int> store = Store<int>(counterReducer, initialState: 0);
Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreProvider<int>(
store: store,
child: StoreConnector<int, String>(
converter: (store) => store.state.toString(),
builder: (context, count) {
return Column(
children: [
Text(count),
StoreConnector<int, VoidCallback>(
converter: (store) {
return () => store.dispatch(Actions.increment);
},
builder: (context, callback) {
return TextButton(
onPressed: callback,
child: Text("Press me!"),
);
},
),
],
);
},
),
);
}
}
The package is provided here.
Mobx
Seemingly more developer friendly, Mobx adds an approach that relies on decorated properties inside an object that tracks the current state.
There are three core concepts with Mobx:
- Observables - reactive data
- Actions - functions that change the data
- Reactions - functions that track the current state in real time
class Counter = CounterBase with _$Counter;
abstract class CounterBase with Store {
@observable
int value = 0;
@action
void increment() {
value++;
}
}
class Counter extends StatefulWidget {
const Counter({Key? key}) : super(key: key);
@override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
final _counter = Counter();
@override
Widget build(BuildContext context) {
return Column(
children: [
Observer(
builder: (_) => Text("$_counter.value"),
),
TextButton(
onPressed: _counter.increment,
child: Text("Press me"),
),
],
);
}
}
The package is provided here.
Scoped Model
This method is recommended in the documentation, it requires almost no additional boilerplate and it implements a number of Flutters features under the hood - such as the Listenable
interface for the sake of data reactivity and the AnimatedBuilder
class.
class CounterModel extends Model {
int _counter = 0;
int get counter => _counter;
void increment() {
_counter++;
notifyListeners();
}
}
class Counter extends StatelessWidget {
Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ScopedModel<CounterModel>(
model: CounterModel(),
child: ScopedModelDescendant<CounterModel>(
builder: (context, child, model) => Column(
children: [
Text("$model.counter"),
TextButton(
onPressed: model.incremennt,
child: Text("Press me"),
),
],
),
),
);
}
}
The package is provided here.
Flutter Hooks
This one should be a familiar sight to React or React Native developers because the package was inspired by their hook system.
Several hooks are provided, with the most prominent one being useState
. Very simple and easy to use.
class Counter extends HookWidget {
const Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final _counter = useState(0);
void increment() {
_counter.value++;
}
return Column(
children: [
Text("$_counter.value"),
TextButton(
onPressed: increment,
child: Text("Press me"),
),
],
);
}
}
The package is provided here.
Provider
As stated by its creator who also created Flutter Hooks, this package is a wrapper around InheritedWidget
that makes the whole process of using it easier and more reusable.
Among other things provider offers vastly simplified allocation and disposal of resources, lazy loading, boilerplate and predefined code snippets to use.
The official Flutter site even showcases this package.
class CounterViewModel extends ChangeNotifier {
int _counter = 0;
int get counter => _counter;
void incrementCounter() {
_counter++;
notifyListeners();
}
}
class Counter extends StatelessWidget {
Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider<CounterViewModel>(
create: (BuildContext context) => CounterViewModel(),
child: Column(
children: [
Consumer<CounterViewModel>(builder: (context, viewModel, child) {
return Text('${viewModel.counter}');
}),
TextButton(
onPressed: Provider.of<CounterViewModel>(context, listen: false).incrementCounter,
child: Text("Press me"),
),
],
),
);
}
}
The package is provided here.
RiverPod
Again by the same creator, RiverPod is an upgrade of sorts to the Provider package.
The package is even able to be used alongside the Flutter hooks, mentioned before.
class Notifier extends StateNotifier<int> {
Notifier() : super(0);
void increment() {
state = state + 1;
}
}
final provider = StateNotifierProvider<Notifier, int>((ref) {
return Notifier();
});
class Counter extends ConsumerWidget {
const Counter({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
var counter = ref.watch(provider);
return Column(
children: [
Text("$counter"),
TextButton(
onPressed: () => ref.read(provider.notifier).increment(),
child: Text(
"Press me",
style: TextStyle(color: Colors.white),
),
),
],
);
}
}
Unlike Provider, no wrapping is needed in the build method but the widget itself has to extend the ConsumerWidget
class.
The package is provided here.
Firebase
Even though it seems like a strange choice, Firebase can also be used for state management as it allows for the tracking of real time data and it provides an authentification and authorization system right out of the box.
One package that comes to mind when mentioning this is FlutterFire.
Conclusion
There are a lot of ways that one can organize their state, by looking at a plethora of those in this post hopefully some will be useful in future endeavours. Most approaches described are very similar but each one shines in its own regard and is used in specific cases, depending on what the user wants to build.
Posted on September 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.