Regretted it? Undo the action
Implement a robust Undo/Redo system in Flutter using the Memento design pattern inside a custom drawing whiteboard.
Time Travel in Flutter ⏳
“It’s like taking a snapshot of the state of something you want to remember later… The great thing is that this ‘photo’ or ‘record’ doesn’t spoil ANYTHING or interfere with what’s happening at that moment.”
The Memento Pattern is a behavioral design pattern that allows you to save and restore the state of an object without exposing its internal details.
There is no better way to demonstrate this than a Drawing Application. Imagine trying to mathematically erase a specific brush stroke that intersects with 5 other strokes. It’s a nightmare. Instead, using the Memento pattern, we just take a “snapshot” of the canvas before drawing. If the user hits undo, we replace the entire canvas with the snapshot!
The Three Pillars of Memento
The pattern requires three specific components:
- Originator: The object whose state needs to be preserved (Our Canvas Controller).
- Memento: The “snapshot” object containing the state (The list of strokes).
- Caretaker: The manager that stores the history of Mementos (Using Undo/Redo Stacks).
Modern Dart Implementation
In modern Flutter applications, the best practice is to capture the entire state into an immutable Memento using List.unmodifiable().
1. The Immutable Memento
class CanvasMemento {
final List<DrawingStroke> strokes;
// ⚠️ CRITICAL: We use List.unmodifiable() to create a deep copy.
// If we just assigned the list, the Memento would point to the
// same memory address as the Canvas, breaking the pattern!
CanvasMemento(List<DrawingStroke> currentStrokes)
: strokes = List.unmodifiable(currentStrokes);
}
2. The Caretaker & Originator
We combine these into a ChangeNotifier to make it reactive. We use two Stacks: _undoStack and _redoStack.
Before any action (starting a new brush stroke, or clearing the board), we take a snapshot:
void _saveSnapshot() {
// Push current state to undo history
_undoStack.add(CanvasMemento(_strokes));
// Any new action invalidates the redo history
_redoStack.clear();
notifyListeners();
}
3. Restoring State (Undo)
To undo, we push our current state to the Redo stack, pop the last state from the Undo stack, and apply it.
void undo() {
if (_undoStack.isEmpty) return;
_redoStack.add(CanvasMemento(_strokes));
final memento = _undoStack.removeLast();
// Restore the snapshot!
_strokes = List.from(memento.strokes);
notifyListeners();
}
By applying this pattern, your drawing app can gracefully recover from bad strokes, accidental clears, or massive changes with a single tap!