Undo and Redo

This section explains how SimpleAppState supports undo and redo operations.

Undo / redo is implemented by integrating the file_state_manager package with the internal state structure of SimpleAppState.


Overview

Undo and redo in SimpleAppState are based on snapshot history.

  • Each finalized state change is stored as a cloned snapshot

  • Undo moves backward in the snapshot history

  • Redo moves forward in the snapshot history

This mechanism relies on:

  • CloneableFile.clone() for snapshot creation

  • FileStateManager for history management

  • replaceDataFrom() for applying restored state


State Snapshots

SimpleAppState implements the CloneableFile interface.

This allows the entire application state to be:

  • Deep-copied using clone()

  • Managed as an immutable snapshot by FileStateManager

Each snapshot represents a complete and self-contained application state at a specific point in time.

Snapshots are used exclusively for undo / redo and are independent of persistence mechanisms such as toDict() and fromDict().


FileStateManager Integration

To enable undo / redo support, register a SimpleAppState instance with FileStateManager.

final state = SimpleAppState();
final fsm = FileStateManager(state, stackSize: 20);

The manager stores cloned snapshots and tracks the current position in the history stack.


State Listener and Push Timing

Undo / redo history must be updated only when a state change is finalized.

For this purpose, SimpleAppState provides a dedicated state listener.

/// Adds a listener suitable for undo / redo management.
/// This listener is notified only when a state change is finalized.
/// Batch updates trigger the notification only once.
void setStateListener(StateListener? listener) {
  _stateListener = listener;
}

A typical integration looks like this:

state.setStateListener((SimpleAppState mState) {
  fsm.push(mState);
});

This design ensures that:

  • Intermediate mutations are not recorded

  • Batch updates produce a single history entry

  • Each undo step corresponds to a meaningful user action


Undo Operation

Calling undo() retrieves the previous snapshot from the history stack.

final previous = fsm.undo();

If undo is possible, a cloned snapshot is returned. The returned snapshot must then be applied to the current state.

Applying a Snapshot Safely

When restoring a snapshot using replaceDataFrom(), the internal state of SimpleAppState is replaced.

This operation finalizes a state change, which would normally trigger the state listener.

To prevent the restored snapshot from being recorded as a new history entry, you must suppress the next push operation.

FileStateManager provides skipNextPush() for this purpose.

final previous = fsm.undo();
fsm.skipNextPush();
state.replaceDataFrom(previous);

Key characteristics of replaceDataFrom():

  • The current SimpleAppState instance remains active

  • Only internal data is replaced

  • No new history entry is created when skipNextPush() is used


Redo Operation

Redo retrieves the next snapshot in the history stack.

final next = fsm.redo();
fsm.skipNextPush();
state.replaceDataFrom(next);

Redo follows the same rules as undo and must also suppress automatic history pushes during restoration.


Complete Example

The following test demonstrates a complete undo / redo flow:

test('undo and redo', () {
  final state = SimpleAppState();
  final intSlot = state.slot<int>('count', initial: 1);
  final fsm = FileStateManager(state, stackSize: 20);

  state.setStateListener((SimpleAppState mState) {
    fsm.push(mState);
  });

  intSlot.set(2);
  intSlot.set(3);

  // undo
  final prev = fsm.undo();
  fsm.skipNextPush();
  state.replaceDataFrom(prev as SimpleAppState);
  expect(intSlot.get(), 2);

  // redo
  final next = fsm.redo();
  fsm.skipNextPush();
  state.replaceDataFrom(next as SimpleAppState);
  expect(intSlot.get(), 3);
});

Design Rationale

This design provides several important guarantees:

  • Undo history reflects logical state changes, not low-level mutations

  • Restored states do not pollute the history stack

  • Undo / redo logic remains decoupled from state mutation logic

  • Slot references remain valid across undo / redo operations

By explicitly controlling push timing, SimpleAppState ensures predictable and intuitive undo / redo behavior.

These operations are also supported by RefAppState. To use them with RefAppState, create a RefAppState with only persistable classes registered in its slots, and then use that as the operation target.