This is my approach to state management in Flutter

How and why I dropped using third party state management dependencies

Over the last 6 years of being active with Flutter I’ve seen lots of solutions to the same problem.

  • MobX
  • Provider
  • Riverpod
  • ScopedModel
  • bloc
  • watch_it
  • Redux

The fun part is that you can make any of these solutions work but what I ended up learning is that you probably shouldn’t.

The downsides of state management packages

Before we dive into how I solve this issue, I would like to talk a bit about the downsides of using large dependencies to solve the issue around “state”.

  1. You have dependency that is usually tightly coupled with your entire application. Meaning if you ever need to migrate to another solution it’s going to be tough.
class BusinessLogicComponent extends Bloc<MyEvent, MyState> {
BusinessLogicComponent(this.repository) {
on<AppStarted>((event, emit) {
try {
final data = await repository.getAllDataThatMeetsRequirements();
emit(Success(data));
} catch (error) {
emit(Failure(error));
}
});
}
final Repository repository;
}
  1. Major changes of the dependency could lead to high work load to make sure your application is using the new approach of that specific dependency. As of writing this a lot of the packages are version 8 and above.

bloc

  1. Not all apps becomes enterprise sized, so the added overhead in smaller apps might impact speed.
  2. Some of these packages require special testing solutions to confidently test your codebase
void main() {
test('riverpod test example', () {
final container = createContainer();
expect(
container.read(provider),
equals('some value'),
);
});
}
  1. Steep learning curves comes with different solutions. A person well versed in Riverpod is not well versed in bloc.

So what is the solution?

The solution is already part of the framework, it’s mainly a matter of how and what approach you want or should take.

Depending on how you look at it there are upsides and downsides to not having clear recommendations but because of that I will give my recommendations.

First, let’s break down what is required from the concept of “state management”.

Some kind of state should mark the widget as dirty and rebuild.

The most clear cut example of this is using setState , though as we all know this is not a scalable approach to building out your state management approach.

There are two different types though.

Ephemeral state

Use this as much as possible, make components that can handle their own state. In essence, the state is self contained.

Some examples of this:

  • A button should be able to block multiple taps if an async call is in action.
  • The progress of a specific animation that can also be reused

App state

There are currently two main approaches (if we ignore streams)

ChangeNotifier and ValueNotifier, both these have builder widgets that will cause rebuilds when any of these require state changes to happen.

ValueListenableBuilder<int>(
valueListenable: _counterPageViewModel.counterNotifier,
builder: (context, counter, child) => Text(
'Counter Value: $counter',
),
),

To provide data throughout your applications there are InheritedWidget and Service locators use whatever you like the most.

For every page you have a View Model, that view models contains the state of that page and notifies whenever something is dirty and needs a rebuild. You can use ChangeNotifier or ValueNotifier where I prefer the latter.

What I do

app wide services is again simple classes provided throughout the application life-cycle that I personally use ValueNotifier ’s for and a service locator.

Let me provide a simple example

I just want to mention that I go much further into this in the “best flutter course on the internet” that me and Tadas have created. But I will provide examples here as well!

Course thumbnail

Want to learn Flutter?

We teach you all you need to confidently build apps at hungrimind.com/learn/flutter

The most simple example I could create for the demonstration would be a “counter feature”.

  1. CounterPage initializes and hold a reference to CounterPageViewModel
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
final CounterPageViewModel _counterPageViewModel = CounterPageViewModel();
@override
Widget build(BuildContext context) {
return ...
}
}
  1. CounterPageViewModel contains a ValueNotifier<int> counterNotifier and relevant methods to increment and decrement
class CounterPageViewModel {
final ValueNotifier<int> counterNotifier = ValueNotifier(0);
void increment() {
counterNotifier.value = counterNotifier.value + 1;
}
void decrement() {
counterNotifier.value = counterNotifier.value - 1;
}
}
  1. The page uses a ValueListenableBuilder to react to the counterNotifier
ValueListenableBuilder<int>(
valueListenable: _counterPageViewModel.counterNotifier,
builder: (context, counter, child) => Text(
'Counter Value: $counter',
),
),
FloatingActionButton(
onPressed: _counterPageViewModel.increment,
child: Text('increment'),
)

if you have other app wide service you dependency inject those with either InheritedWidget or a Service Locator.

final TodoPageViewModel _todoPageViewModel = TodoPageViewModel(
myService: locator<SomeService>(),
);

This means that view models doesn’t need to be provided with InheritedWidget or similar, you would just pass it down the tree as pages usually have a limited depth.

This is fully scalable

This approach is fully scalable, doesn’t rely on large dependencies and anyone that use Flutter can understand without much ramp up time.

To summarize

  1. Use Ephemeral state as much as possible
  2. Use ChangeNotifier or ValueNotifiers
  3. Use different ListenableBuilders depending on the notifier
  4. Keep it simple

You don’t need to complicate your setup even if you want to scale to multiple hundred thousand lines (or even millions).

Get articles right in your inbox

No spam, unsubscribe anytime. We treat your inbox with respect.