Flutter Tutorial

Flutter Intermediate Lesson 57 of 135 3 min read

Redux in Flutter: Store, Actions, Reducers and StoreConnector

Learn Redux in Flutter with flutter_redux: store, actions, pure reducers, StoreProvider, StoreConnector and middleware, with easy counter and to-do code.

On this page

Redux keeps all app state in one place, the store. The UI never changes state directly. Instead it dispatches actions (“increment”, “add todo”), and a pure function called a reducer turns the old state and the action into the new state.

UI → dispatch(action) → reducer(oldState, action) → newState → UI rebuilds

This one-way flow makes every change predictable and easy to log, test and debug. Redux came from the JavaScript world.

The packages are not available in the online editor, so run the examples in your own project.

Install

flutter pub add redux flutter_redux

The counter

import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';

// 1. Actions: what happened
enum CounterAction { increment, reset }

// 2. Reducer: (state, action) -> new state. No side effects.
int counterReducer(int state, dynamic action) {
  if (action == CounterAction.increment) return state + 1;
  if (action == CounterAction.reset) return 0;
  return state;
}

void main() {
  // 3. Store: holds the state
  final store = Store<int>(counterReducer, initialState: 0);
  runApp(CounterApp(store: store));
}

class CounterApp extends StatelessWidget {
  const CounterApp({super.key, required this.store});
  final Store<int> store;

  @override
  Widget build(BuildContext context) {
    // 4. StoreProvider makes the store available below it
    return StoreProvider<int>(
      store: store,
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Redux')),
          body: Center(
            // 5. StoreConnector reads part of the state and rebuilds when it changes
            child: StoreConnector<int, String>(
              converter: (store) => store.state.toString(),
              builder: (context, count) => Text(count, style: const TextStyle(fontSize: 48)),
            ),
          ),
          floatingActionButton: StoreConnector<int, VoidCallback>(
            converter: (store) => () => store.dispatch(CounterAction.increment),
            builder: (context, increment) => FloatingActionButton(
              onPressed: increment,
              child: const Icon(Icons.add),
            ),
          ),
        ),
      ),
    );
  }
}

A real state object

Real apps keep one immutable AppState class and use action classes that carry data:

class AppState {
  const AppState({this.todos = const []});
  final List<String> todos;
  AppState copyWith({List<String>? todos}) => AppState(todos: todos ?? this.todos);
}

class AddTodo {
  const AddTodo(this.title);
  final String title;
}

class RemoveTodo {
  const RemoveTodo(this.index);
  final int index;
}

AppState appReducer(AppState state, dynamic action) {
  if (action is AddTodo) {
    return state.copyWith(todos: [...state.todos, action.title]);
  }
  if (action is RemoveTodo) {
    return state.copyWith(todos: [...state.todos]..removeAt(action.index));
  }
  return state;
}

final store = Store<AppState>(appReducer, initialState: const AppState());
store.dispatch(const AddTodo('Learn Redux'));

Async work: middleware

Reducers must stay pure, so API calls go in middleware. The redux_thunk package lets you dispatch a function:

// flutter pub add redux_thunk
ThunkAction<AppState> loadTodos() => (Store<AppState> store) async {
      final todos = await api.fetchTodos();
      for (final t in todos) {
        store.dispatch(AddTodo(t));
      }
    };

final store = Store<AppState>(
  appReducer,
  initialState: const AppState(),
  middleware: [thunkMiddleware],
);
store.dispatch(loadTodos());

Pros and cons

  • Pros: every change goes through one place; easy to log, undo and test; familiar to React developers.
  • Cons: a lot of boilerplate for small apps; one big state object can feel heavy; less common in new Flutter projects than Riverpod or Bloc.

Try it yourself

Add a ToggleTodo action and a done flag to each to-do, and show the number of finished tasks with a StoreConnector<AppState, int>.

Practise in the playground Updated