Flutter Redux
Predictable state container with time-travel debugging
🔮 What is Redux?
Redux is a predictable state container using a single source of truth. Actions dispatch changes, reducers process them, and the store holds app state. It enables time-travel debugging and makes state changes traceable and predictable.
// Dispatch an action
store.dispatch(IncrementAction());
Key Concepts
Store
Single source of truth
final store = Store<AppState>(
reducer)
Actions
Describe what happened
class IncrementAction {}
Reducers
Pure functions that update state
AppState reducer(
AppState state, action)
Time Travel
Debug by replaying actions
// Undo/redo capability
// Action history tracking
🔹 Installation
Add Redux packages to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
flutter_redux: ^0.10.0
redux: ^5.0.0
Then run:
flutter pub get
🔹 Basic Counter Example
Create a counter using Redux:
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
// 1. Define State
class AppState {
final int counter;
AppState({required this.counter});
AppState copyWith({int? counter}) {
return AppState(counter: counter ?? this.counter);
}
}
// 2. Define Actions
class IncrementAction {}
class DecrementAction {}
// 3. Create Reducer
AppState counterReducer(AppState state, dynamic action) {
if (action is IncrementAction) {
return state.copyWith(counter: state.counter + 1);
} else if (action is DecrementAction) {
return state.copyWith(counter: state.counter - 1);
}
return state;
}
// 4. Create Store
final store = Store<AppState>(
counterReducer,
initialState: AppState(counter: 0),
);
Output:
Redux store setup with state, actions, and reducer for a counter.
🔹 Using Redux in UI
Connect Redux store to Flutter widgets:
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Provide store to widget tree
return StoreProvider<AppState>(
store: store,
child: MaterialApp(
home: CounterPage(),
),
);
}
}
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Redux Counter')),
body: Center(
// Connect to store
child: StoreConnector<AppState, int>(
converter: (store) => store.state.counter,
builder: (context, counter) {
return Text(
'$counter',
style: TextStyle(fontSize: 48),
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: () {
// Dispatch action
StoreProvider.of<AppState>(context)
.dispatch(IncrementAction());
},
child: Icon(Icons.add),
),
SizedBox(height: 10),
FloatingActionButton(
onPressed: () {
StoreProvider.of<AppState>(context)
.dispatch(DecrementAction());
},
child: Icon(Icons.remove),
),
],
),
);
}
}
Output:
A counter app with increment and decrement buttons using Redux.
🔹 Complex State Example
Managing multiple pieces of state:
// Complex state
class AppState {
final int counter;
final List<String> todos;
final bool isLoading;
AppState({
required this.counter,
required this.todos,
required this.isLoading,
});
AppState copyWith({
int? counter,
List<String>? todos,
bool? isLoading,
}) {
return AppState(
counter: counter ?? this.counter,
todos: todos ?? this.todos,
isLoading: isLoading ?? this.isLoading,
);
}
}
// Multiple actions
class AddTodoAction {
final String todo;
AddTodoAction(this.todo);
}
class SetLoadingAction {
final bool isLoading;
SetLoadingAction(this.isLoading);
}
// Combined reducer
AppState appReducer(AppState state, dynamic action) {
if (action is IncrementAction) {
return state.copyWith(counter: state.counter + 1);
} else if (action is AddTodoAction) {
return state.copyWith(
todos: [...state.todos, action.todo],
);
} else if (action is SetLoadingAction) {
return state.copyWith(isLoading: action.isLoading);
}
return state;
}
Output:
A more complex Redux setup managing multiple state properties.
🔹 Middleware Example
Add middleware for logging and async operations:
// Logging middleware
void loggingMiddleware(
Store<AppState> store,
dynamic action,
NextDispatcher next,
) {
print('Action: $action');
print('State before: ${store.state.counter}');
next(action); // Pass to next middleware or reducer
print('State after: ${store.state.counter}');
}
// Async middleware for API calls
void asyncMiddleware(
Store<AppState> store,
dynamic action,
NextDispatcher next,
) {
if (action is FetchDataAction) {
store.dispatch(SetLoadingAction(true));
// Simulate API call
Future.delayed(Duration(seconds: 2), () {
store.dispatch(SetLoadingAction(false));
store.dispatch(AddTodoAction('Fetched data'));
});
}
next(action);
}
// Create store with middleware
final store = Store<AppState>(
appReducer,
initialState: AppState(
counter: 0,
todos: [],
isLoading: false,
),
middleware: [loggingMiddleware, asyncMiddleware],
);
Output:
Redux store with middleware for logging and handling async operations.
🔹 Selectors for Derived Data
Compute derived state efficiently:
// Selector functions
int selectCounter(AppState state) => state.counter;
List<String> selectTodos(AppState state) => state.todos;
int selectTodoCount(AppState state) => state.todos.length;
bool selectHasTodos(AppState state) => state.todos.isNotEmpty;
// Use in StoreConnector
class TodoCountWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, int>(
converter: (store) => selectTodoCount(store.state),
builder: (context, todoCount) {
return Text('Total todos: $todoCount');
},
);
}
}
Output:
Selector functions that compute derived data from the Redux state.
🔹 When to Use Redux
✅ Good For:
- Large apps with complex state
- Apps requiring time-travel debugging
- Teams familiar with Redux from web
- Apps with many state interactions
- When you need middleware for logging/analytics
❌ Consider Alternatives For:
- Simple apps (use setState or Provider)
- Beginners to state management
- When boilerplate is a concern
- Apps without complex state interactions
🔹 Redux Principles
Three Core Principles:
- Single source of truth: One store holds entire app state
- State is read-only: Only way to change state is to dispatch actions
- Pure reducers: Reducers are pure functions with no side effects
Benefits:
- Predictable state updates
- Easy to test and debug
- Time-travel debugging capability
- Centralized state management