Essentials

Undo & Redo

Add undo/redo history to any shard with HistoryMixin.

Overview

HistoryMixin<T> adds undo/redo history to any Shard<T>. Mix it in and every state change becomes reversible—no extra wiring, no separate package.

Each state change (via emit() or emitForce()) pushes the previous state onto an undo stack and clears the redo stack. Calling undo() restores that previous state and moves the current state onto the redo stack; redo() re-applies it.

class DocumentShard extends Shard<Document> with HistoryMixin<Document> {
  DocumentShard() : super(Document.empty());
  void edit(Document next) => emit(next);
}

final doc = DocumentShard()..edit(a)..edit(b);
doc.canUndo; // true
doc.undo();  // back to a
doc.redo();  // forward to b

Undo and redo restore state via setStateInternal, so restores are not re-recorded—there's no runaway growth of the history stacks. They still notify listeners and the global observer, so your UI and any ShardObserver react exactly as they would to a normal change.

API surface

HistoryMixin<T> adds the following to your shard:

  • void undo() – Restore the previous state, moving the current state onto the redo stack. No-op when there is nothing to undo.
  • void redo() – Re-apply the most recently undone state. No-op when there is nothing to redo.
  • bool get canUndotrue when there is at least one state to undo to.
  • bool get canRedotrue when there is at least one state to redo to.
  • void clearHistory() – Clear both the undo and redo stacks.
  • int maxHistory – Settable field, default 50. When the undo stack exceeds this limit, the oldest entry is dropped.
class DocumentShard extends Shard<Document> with HistoryMixin<Document> {
  DocumentShard() : super(Document.empty()) {
    maxHistory = 100; // keep more history than the default 50
  }
  void edit(Document next) => emit(next);
}
undo() and redo() are safe to call at any time—they are no-ops when their stack is empty. You don't need to guard them, but use canUndo/canRedo to drive your UI state.

Wiring undo/redo to the UI

Use canUndo and canRedo to enable or disable buttons reactively. Because restores notify listeners, a ShardBuilder rebuilds whenever the available history changes:

// Enable/disable buttons reactively from canUndo/canRedo:
ShardBuilder<DocumentShard, Document>(
  builder: (context, _) {
    final shard = context.read<DocumentShard>();
    return Row(children: [
      IconButton(
        icon: const Icon(Icons.undo),
        onPressed: shard.canUndo ? shard.undo : null,
      ),
      IconButton(
        icon: const Icon(Icons.redo),
        onPressed: shard.canRedo ? shard.redo : null,
      ),
    ]);
  },
)

Passing null to onPressed disables the IconButton automatically, so the buttons grey out when there's nothing to undo or redo.

Combining with persistence

HistoryMixin and StatePersistenceMixin are orthogonal and can be mixed into the same shard. They hook into different points: HistoryMixin overrides onChange (to record history) while StatePersistenceMixin overrides emit (to save state).

The two mixins are orthogonal, but they touch overlapping lifecycle methods. If you rely on both undo/redo and persistence in the same shard, test the combination to confirm the ordering behaves the way you expect.

Next Steps

Next: Testing.