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>.