What State Management is all about (in practice)

Sandro Maglione

Sandro Maglione

Mobile development

This week is a deep dive in everything about State management.

I tried 4 different packages (riverpod, bloc, signals,Β get) to understand how they all work and what solution they propose.

Here is everything that I learned πŸ‘‡


Tech stack

  • Flutter: I explored state management for flutter apps, but the principles of how state management works apply to every frontend framework πŸ‘€

Setup

The project is a normal Flutter app. I created a separate folder and main.dart for each state management package:

Each folder is specific for each state management package, and each has its own entry file main.dartEach folder is specific for each state management package, and each has its own entry file main.dart

In this way I can run each main.dart to test the app for every package. The apps look the same, but they all implement a different state management solution.

Compose words using the letters in the grid. The app needs to manage the state for the current selection, the words dictionary, the found words, and the points πŸ€”Compose words using the letters in the grid. The app needs to manage the state for the current selection, the words dictionary, the found words, and the points πŸ€”

Let's see how this all works!

Get started

What is state management really? πŸ€”

State Management is the process of dealing with state changes over time: store initial value, read current value, update value

Every time some state changes the UI should rerender.

In summary:

  • Store a variable somewhere (State)
  • Access the current value of the variable (UI)
  • Update the variable (Events)
  • πŸ‘‰ Reflect these changes in the UI

The last point is tricky πŸ€”

The update should involve the smallest subset of UI possible to avoid rerendering the full screen after every state change 🀯

This is where a state management library comes to the rescue πŸš€

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.

Implementation

First problem: where is the state stored? In what format?

A common practice is to make the state immutable:

Immutable state allows the state management library to track changes over time. Just compare the previous value with the updated one: if the value is different, rerender the UI

This is the approach used by riverpod, bloc, and signals:

part of 'dictionary_bloc.dart';
 
@immutable /// πŸ‘ˆ Make the state immutable 
sealed class DictionaryState {
  const DictionaryState();
}

When the state is not immutable you need a way to notify the UI that something changed. GetX requires you to manually call an update method:

void onPanStart(GridSettings gridSettings, DragStartDetails details) {
  final pos = _panIndex(gridSettings, details.localPosition);
 
  gesture = gesture.add(pos.index); /// πŸ‘ˆ State changed
  update(); /// πŸ‘ˆ Manually notify the UI
}

Surgical Renderingℒ️

Second problem: how do you access the state? Which part of the UI should be rerendered?

A common solution in Flutter is providing a widget that tracks state changes and rerenders itself after every update

This is how providers and builders work:

class Grid extends StatelessWidget {
  const Grid({super.key});
 
  @override
  Widget build(BuildContext context) {
    /// πŸ‘‡ [Watch] widget listens for changes and rerenders on every update
    return Watch(
      (context) {
        final dictionaryAsync = dictionary.value;

Another approach is to read the state directly from context (ref.watch). This method will rerender the full widget after every update (and not just a subset of the tree):

class Grid extends StatelessWidget {
  const Grid({super.key});
 
  @override
  Widget build(BuildContext context) {
    /// Watch values and rerender the full [Grid] after every update
    final gestureBloc = context.read<GestureBloc>();
    final gestureBlocState = context.watch<GestureBloc>().state;
    final gridSettings = context.watch<GridSettings>();
    final boardBloc = context.watch<BoardBloc>();
    return GridLayout(

Update events

Is the state allowed to change from any value to any other value? πŸ€”

It is common for a UI to be modelled as a state machine:

No one of the package I tried implement this pattern πŸ’πŸΌβ€β™‚οΈ

part 'gesture_provider.g.dart';
 
@riverpod
class GestureNotifier extends _$GestureNotifier {
  @override
  Gesture build() => Gesture.empty();
 
  void onPanStart(GridSettings gridSettings, DragStartDetails details) {
    final pos = _panIndex(gridSettings, details.localPosition);
    state = state.add(pos.index); /// πŸ‘ˆ No limits on how can you update the state
  }

Pay attention when you update the state: make sure the event is allowed and the data is valid


Every state management solution has its own tradeoffs, which one to choose mostly depends on your project πŸ‘‡

I wrote a complete guide on state management in Flutter using riverpod, bloc, signals, and GetX.

This guide covers everything that you need to know about state management with each package and how they work in practice πŸš€

Takeaways

  • State management is about storing, reading, and updating data
  • State management libraries are responsible to update a subset of the UI after each state change
  • There is no "best" solution: each package has its own patterns and features (caching, devtools, immutability)
  • Since the problem solved is the same, your app will work regardless of the package that you choose, so go ahead and pick your favorite 🀝

State management is an hard problem. Knowing the principles and the most common solutions helps when working on your app.

Make sure to try many libraries and choose the best tool for the job πŸ› οΈ

See you next πŸ‘‹

Start here.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.