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.
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 canUndo – true when there is at least one state to undo to.bool get canRedo – true 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.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.
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).
Next: Testing.