Skip to main content

Undo and Redo

AsyncRedux makes it easy to add undo and redo features to your app. The approach is straightforward:

  1. Create a state observer to save states to a history list
  2. Create actions to navigate through that history (undo/redo)

This also works to undo/redo only part of the state. Your observer can save only that part, and your actions can revert only that part.

Example Implementation​

1. Create the UndoRedoObserver​

The observer saves each state change to a history list:

class UndoRedoObserver implements StateObserver<AppState> {
final List<AppState> _history = [];
int _currentIndex = -1;

// Maximum number of states to keep in history
final int maxHistorySize;

UndoRedoObserver({this.maxHistorySize = 50});

void observe(
ReduxAction<AppState> action,
AppState prevState,
AppState newState,
Object? error,
int dispatchCount,
) {
// Don't save states from undo/redo actions themselves
if (action is UndoAction || action is RedoAction) return;

// If state didn't change, don't add to history
if (identical(prevState, newState)) return;

// Remove any "future" states if we're not at the end
if (_currentIndex < _history.length - 1) {
_history.removeRange(_currentIndex + 1, _history.length);
}

// Add the new state
_history.add(newState);
_currentIndex = _history.length - 1;

// Limit history size
if (_history.length > maxHistorySize) {
_history.removeAt(0);
_currentIndex--;
}
}

bool get canUndo => _currentIndex > 0;

bool get canRedo => _currentIndex < _history.length - 1;

AppState? getPreviousState() {
if (!canUndo) return null;
_currentIndex--;
return _history[_currentIndex];
}

AppState? getNextState() {
if (!canRedo) return null;
_currentIndex++;
return _history[_currentIndex];
}
}

2. Register the Observer with the Store​

final undoRedoObserver = UndoRedoObserver();

var store = Store<AppState>(
initialState: AppState.initialState(),
stateObservers: [undoRedoObserver],
);

3. Create Undo and Redo Actions​

class UndoAction extends ReduxAction<AppState> {
AppState? reduce() {
AppState? previousState = undoRedoObserver.getPreviousState();
return previousState ?? null;
}
}

class RedoAction extends ReduxAction<AppState> {
AppState? reduce() {
AppState? nextState = undoRedoObserver.getNextState();
return nextState ?? null;
}
}

4. Use in Your UI​

class UndoRedoButtons extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
icon: Icon(Icons.undo),
onPressed: () => context.dispatch(UndoAction()),
),
IconButton(
icon: Icon(Icons.redo),
onPressed: () => context.dispatch(RedoAction()),
),
],
);
}
}