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

🧠 Test Your Knowledge

What are the three main parts of Redux?