skip to content

Flutter Redux

flutter_redux connects a Dart Redux Store to widgets: StoreProvider exposes it and StoreConnector maps state to a view model through a converter. Interviewers ask why Redux is rare in Flutter.

on this pageshow

explore

questions

5

In a Flutter app using redux and flutter_redux, what are the Store, a reducer and StoreProvider, and how does a dispatched action reach a widget?

level: juniorimportance: must knowfreq 38%

answer

  1. one Store, created before runApp
  2. reducer: (state, action) returns new state
  3. StoreProvider above MaterialApp
  4. full generic type on every widget
  5. onChange stream drives the rebuild

basics

~20 s

The Store holds all app state and is built from a reducer, an initialState and middleware; a reducer maps state and action to the next state. StoreProvider exposes the Store; after a dispatch the Store emits onChange and connected widgets rebuild.

solid answer

~40 s

With the `redux` package you create one `Store<AppState>(appReducer, initialState: ..., middleware: [...])` before `runApp`. A **reducer** is a plain Dart function `AppState reducer(AppState state, dynamic action)` that returns the next state without side effects; `combineReducers` and `TypedReducer` split it by action type. flutter_redux's `StoreProvider<AppState>` is an `InheritedWidget` you place above `MaterialApp`, so every route can reach the store through `StoreProvider.of<AppState>(context)`, `StoreConnector` or `StoreBuilder`. When a widget calls `store.dispatch(action)`, middleware sees it first, then the reducer computes the new state, the Store emits on its `onChange` stream, and each `StoreConnector` listening to that stream converts the state and rebuilds. Type arguments matter: a missing `<AppState>` makes the lookup fail with `StoreProviderError`.

code

dart · 23 lines
dart
import 'package:flutter/widgets.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';

class ItemScanned {
  ItemScanned(this.priceCents);
  final int priceCents;
}

class ScanButton extends StatelessWidget {
  const ScanButton({super.key, required this.priceCents});
  final int priceCents;

  @override
  Widget build(BuildContext context) {
    // listen: false - this widget only dispatches, it does not display state.
    final Store<int> store = StoreProvider.of<int>(context, listen: false);
    return GestureDetector(
      onTap: () => store.dispatch(ItemScanned(priceCents)),
      child: const Text('Scan'),
    );
  }
}

go deeper

for a junior

Know the four roles: one Store built from a reducer and initialState, a reducer that returns the next state, StoreProvider at the top of the tree, and dispatch to change state.

for a middle

Explain how a dispatch travels through middleware, reducer and onChange to a StoreConnector, and why StoreProvider itself notifies only when the store instance changes.

for a senior

Show you can debug a StoreProviderError from type information and placement, and keep side effects out of reducers in a real legacy codebase.

for a principal

Assess the cost of one global store in a large app — every dispatch fans out to every connector — and how that shapes module boundaries.

## The pieces, in Flutter terms Redux in Flutter is two packages: **`redux`**, which provides the `Store` and the reducer and middleware plumbing, and **`flutter_redux`** (pinned here at 0.6.0), which connects a `Store` to widgets. In a legacy point-of-sale app, the single store might hold the current cart, the selected customer and the payment status. | Piece | Package | What it is | |---|---|---| | `Store<S>` | `redux` | holds the one state object, runs reducers, exposes `state`, `dispatch` and the `onChange` stream | | reducer | `redux` | `S Function(S state, dynamic action)` returning the next state | | middleware | `redux` | code that sees every action before the reducer, for side effects | | `StoreProvider<S>` | `flutter_redux` | an `InheritedWidget` that exposes the store to descendants | | `StoreConnector<S, VM>` / `StoreBuilder<S>` | `flutter_redux` | widgets that read the store and rebuild on changes | ## Creating the Store The store is created once, outside `build`, usually in `main`: - the **reducer** is the first positional argument; - **`initialState`** is a named argument and becomes `store.state` before any dispatch; - **`middleware`** is an optional list run on every dispatch, in order, before the reducer. ## Reducers in Dart A reducer takes the current state and an action and returns the next state. Actions are typed `dynamic` in the signature, so Dart code distinguishes them by type. Two approaches work: 1. `combineReducers<PosState>([TypedReducer<PosState, ItemScanned>(_onScan), ...])` — each `TypedReducer` handles one action class. 2. In Dart 3, a `sealed` action hierarchy and a `switch` expression with patterns, which gives exhaustiveness checking over the actions. The reducer must not call APIs or mutate the old state; side effects belong in middleware. ## Providing the Store to the tree `StoreProvider<PosState>(store: store, child: MaterialApp(...))` wraps the app. Because it is an `InheritedWidget`, any descendant can find it: - `StoreProvider.of<PosState>(context)` returns the store and, by default (`listen: true`), registers the widget as a dependent. - `StoreProvider.of<PosState>(context, listen: false)` looks it up without registering a dependency, which is what you want in `initState`. - Its `updateShouldNotify` compares store identity, so dependents are notified only if a different `Store` instance is provided — not on every state change. That last point is why widgets do not rebuild through `StoreProvider` alone: state changes arrive through the store's **`onChange`** stream, which `StoreConnector` listens to. ## The trip of one action 1. A button calls `store.dispatch(ItemScanned(item))`. 2. Each middleware runs and passes the action on. 3. The reducer returns a new `PosState` with the item added. 4. The store emits the new state on `onChange`. 5. Every `StoreConnector` re-runs its `converter` and rebuilds its subtree. ## The type-information trap `StoreProvider.of<S>` looks up `StoreProvider<S>` by its exact type. If the provider was created as `StoreProvider(store: store, ...)` without a type argument, or a connector uses a different state type, the lookup fails and flutter_redux throws `StoreProviderError`, whose message advises wrapping `MaterialApp` rather than one route and providing full type information to `Store`, `StoreProvider` and `StoreConnector`. ```dart import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; sealed class PosAction {} class ItemScanned extends PosAction { ItemScanned(this.priceCents); final int priceCents; } class CartCleared extends PosAction {} int totalReducer(int totalCents, dynamic action) => switch (action) { ItemScanned(:final priceCents) => totalCents + priceCents, CartCleared() => 0, _ => totalCents, }; void main() { final store = Store<int>(totalReducer, initialState: 0); runApp(StoreProvider<int>( store: store, child: const MaterialApp(home: Placeholder()), )); } ```

  • Why does StoreProvider not rebuild its dependents when the state changes?
    flutter_redux's `StoreProvider.updateShouldNotify` compares only the `Store` instance, and the store object stays the same while its state changes. State changes are delivered through the store's `onChange` stream, which `StoreConnector` and `StoreBuilder` listen to; they are what rebuild.
  • What causes StoreProviderError, and how do you fix it?
    `StoreProvider.of<S>` finds the provider by exact generic type. A provider created without `<AppState>`, a connector typed with another state class, or a provider wrapped around a single route instead of `MaterialApp` all make the lookup fail. Give every `Store`, `StoreProvider` and `StoreConnector` the same full type, and place the provider above `MaterialApp`.

saying these in an interview costs you the question

  • Each screen should create its own Store in its build method.
  • Reducers can call the payment API because they receive the action.
  • StoreProvider rebuilds every dependent widget whenever the state changes.
  • StoreProvider(store: store) without a type argument works the same as StoreProvider<AppState>.
  • Middleware runs after the reducer has already produced the new state.
open as a page

In flutter_redux, what does StoreConnector's converter do, and why must the view model implement == when distinct is true?

level: middleimportance: must knowfreq 34%

basics

~20 s

The converter maps the Store to a small view model and runs on every store change. With distinct: true, StoreConnector rebuilds only when the new view model != the previous one, so the view model needs value equality.

open as a page

Why is flutter_redux uncommon in current Flutter codebases, and what would you check before extending a legacy point-of-sale app built on it?

level: seniorimportance: should knowfreq 22%

basics

~10 s

flutter_redux needs one global store, a converter and value-equal view model per widget, dynamic actions and extra middleware for async work. Before extending a legacy app, check the package version, rebuild cost and conventions.

open as a page

In a Flutter redux app, how do you run async work such as a card-payment call through middleware, and what does a thunk middleware change?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Middleware sees each action before the reducer: on a charge action it calls next(action), starts the payment future and later dispatches a success or failure action. A thunk middleware lets you dispatch a function that receives the store.

open as a page

In flutter_redux, when do StoreConnector's onInit, onWillChange, onDidChange and onInitialBuild run, and which one should trigger navigation?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

onInit runs in initState before the first view model is built, onWillChange runs on a change before the rebuild, onDidChange and onInitialBuild run after the frame via post-frame callbacks, and onDispose runs in dispose. Navigation belongs in onWillChange.

open as a page