Dart Tutorial

Flutter Lesson 43 of 83 4 min read

Choosing a State Management Approach in Flutter

Compare setState, ChangeNotifier, Provider, Riverpod and Bloc for Flutter, and learn how to choose the right one for your project.

On this page

Beginners often lose weeks choosing a state management package before writing an app. Every option in this chapter can build a large, successful app. The choice matters less than using one of them consistently.

The same job, five ways #

All of them do two things: make state reachable from where it is needed, and rebuild the widgets that use it.

ApproachState lives inWidget reads it withChange it with
setStateA State objectFieldssetState(() {...})
ChangeNotifierA class you writeListenableBuilderMethods that call notifyListeners()
ProviderA ChangeNotifier placed in the treecontext.watch<T>()context.read<T>().method()
RiverpodA Notifier behind a global providerref.watch(provider)ref.read(provider.notifier).method()
BlocA Cubit or Bloc placed in the treeBlocBuildercubit.method() or bloc.add(Event())

Comparison #

setStateChangeNotifierProviderRiverpodBloc
Extra packageNoNoYesYesYes
Learning curveLowestLowLowMediumMedium to high
BoilerplateNoneLittleLittleLittle to mediumMost
Shares state across screensAwkwardWith a global or inherited widgetYesYesYes
Async loading and error statesBy handBy handBy handBuilt inModelled as states
Compile-time safetyn/an/aRun-time errors possibleStrongStrong
Testing logic without UIHardEasyEasyEasyEasiest
Common inEvery app, for local stateSmall apps, official samplesMany existing appsNewer appsLarger teams and companies

A way to decide #

Is the state used by one widget only? Use setState. A checkbox, the current tab and an expanded panel do not need a package, whatever else the app uses.

Is it a small app, or are you still learning? Use ChangeNotifier, on its own or with Provider. There is the least to learn, and the ideas carry over to everything else.

Is it a new app that will grow, with a lot of async data? Riverpod is a strong choice. AsyncValue, provider dependencies and automatic disposal remove a lot of hand-written code.

Is it a large app with several developers, where consistency and traceability matter most? Bloc. Its strict structure means every feature is written the same way.

Joining an existing project? Use what it already uses. Mixing approaches in one codebase costs more than any individual tool saves.

What about GetX, MobX, signals and the rest? #

There are many more packages. Some are popular, particularly GetX, which bundles state, routing and dependency injection with very little code. Before adopting any package for the core of your app, check that it is actively maintained, well documented, and does not hide so much that debugging becomes guesswork. The five approaches in this chapter are the safest choices for employability, since most job listings ask for Provider, Riverpod or Bloc.

Principles that matter more than the package #

These apply whichever tool you pick.

1. Keep logic out of widgets. Widgets display state and forward user actions. Decisions, calculations and API calls belong in a model, notifier or cubit.

2. One source of truth. Each piece of data lives in exactly one place. Do not copy it into a second variable that can drift out of step. Derive values instead: total is computed from items, never stored separately.

3. Make state immutable where practical. Replace values instead of changing them in place. It prevents a whole class of “the screen did not update” bugs.

4. Model the states of a screen explicitly. Loading, loaded, empty and failed are four different things. A sealed class says so, where three booleans and a nullable list allow impossible combinations.

5. Keep state as local as possible. Not everything is app state. Form input, scroll position and animation progress belong to the widget.

6. Rebuild small. Watch state in the smallest widget that needs it, and listen to a part of a model where you can.

7. Dispose what you create. Controllers, subscriptions and notifiers.

A layered structure that suits any of them #

UI (widgets)
   | user actions            ^ state
   v                         |
State holder (ChangeNotifier / Notifier / Cubit)
   | calls                   ^ data
   v                         |
Repository (decides: network or cache?)
   |
   v
Services (HTTP client, database, device APIs)

The state management package only fills the second box. The architecture chapter covers the rest.

Do not over-engineer #

A common beginner mistake is setting up Bloc with events, states, repositories and dependency injection for a three-screen app. Start with the simplest thing that works. When a concrete problem appears, such as passing data through five constructors, introduce the tool that solves that problem.

Try it yourself #

Take the cart example from this chapter and write it three times: with ChangeNotifier and ListenableBuilder, with Provider, and with either Riverpod or Cubit. Note which version you find easiest to read. That is useful information about which tool suits you.

Practise in the playground Updated by Santosh Adhikari