Storable Values

This chapter explains what kinds of values can be safely stored in SimpleAppState slots, and how to handle custom classes correctly.

Understanding these rules is essential for:

  • predictable rebuild behavior

  • correct undo / redo

  • reliable persistence

  • avoiding subtle runtime errors

If you only store primitive values, you can skim this chapter. If you want to store your own classes, read it carefully.


The Core Rule

All values stored in SimpleAppState must be safely deep-copyable.

SimpleAppState enforces value semantics. Every value is copied on:

  • slot definition

  • set / update

  • get

  • snapshot / restore

If a value cannot be deep-copied safely, it cannot be used as application state.


Values That Work Out of the Box

The following values are supported without any additional work:

  • primitive values (int, double, bool, String, null)

  • Lists composed recursively of supported values

  • Maps composed recursively of supported values

Examples:

final count = appState.slot<int>('count', initial: 0);

final tags = appState.slot<List<String>>(
  'tags',
  initial: [],
  caster: (raw) => (raw as List).cast<String>(),
);

final config = appState.slot<Map<String, dynamic>>(
  'config',
  initial: {},
  caster: (raw) => (raw as Map).cast<String, dynamic>(),
);

These values are:

  • immutable or safely copied

  • JSON-serializable in principle

  • safe for persistence and undo / redo


When You Need a Custom Class

If a value cannot be represented purely using primitives, Lists, and Maps, it must be wrapped in a custom class.

Typical examples that require a custom class:

  • DateTime

  • Color

  • Offset, Rect, Size

  • Any Flutter framework class

  • Domain objects with identity or behavior

Typical examples that do not require a custom class:

  • int, double, bool, String

  • List<String>

  • Map<String, dynamic> composed only of primitives


CloneableFile

Any custom class stored in a slot must extend CloneableFile from the file_state_manager package.

This allows SimpleAppState to:

  • deep-copy the value automatically

  • serialize it for persistence

  • restore it for undo / redo

  • detect structural changes reliably


Required Methods

A class extending CloneableFile must implement all of the following:

1. clone()

Returns a deep copy of the object.

  • All properties must be copied

  • Nested CloneableFile objects must also be cloned

  • No references to the original object may remain

2. toDict()

Returns a Map<String, dynamic> representation.

The returned map may contain only:

  • primitive values

  • null

  • Lists or Maps composed recursively of the above

If a property is another custom object, it must also extend CloneableFile and be converted via toDict().

3. factory fromDict(Map<String, dynamic> src)

Reconstructs the object from the dictionary produced by toDict().

This method must:

  • restore all properties

  • fully reconstruct the object

  • be deterministic



Example

A safe wrapper for DateTime:

class AppTimestamp extends CloneableFile {
  final int epochMillis;

  AppTimestamp(this.epochMillis);

  factory AppTimestamp.fromDateTime(DateTime dt) {
    return AppTimestamp(dt.millisecondsSinceEpoch);
  }

  DateTime toDateTime() =>
      DateTime.fromMillisecondsSinceEpoch(epochMillis);

  @override
  AppTimestamp clone() => AppTimestamp(epochMillis);

  @override
  Map<String, dynamic> toDict() => {
        'epochMillis': epochMillis,
      };

  factory AppTimestamp.fromDict(Map<String, dynamic> src) {
    return AppTimestamp(src['epochMillis'] as int);
  }

  @override
  bool operator ==(Object other) =>
      other is AppTimestamp && other.epochMillis == epochMillis;

  @override
  int get hashCode => epochMillis.hashCode;
}

This class is:

  • safely copyable

  • serializable

  • undo / redo friendly


Deep Copy Semantics

Classes extending CloneableFile are automatically deep-copied by SimpleAppState, even when nested inside Lists or Maps.

Inside slot.update():

  • the value you receive is already a copy

  • you must mutate it directly and return it

  • do not create additional copies

Example:

timestampSlot.update((_) {
   return AppTimestamp.fromDateTime(DateTime.now());
});

What Must Never Be Stored

The following objects must never be stored in slots, even if wrapped in a custom class:

  • BuildContext

  • FocusNode

  • Stream or Future

  • callbacks or closures

  • controllers, animations, or UI handles

  • any object tied to the widget lifecycle

These objects represent runtime behavior, not application state.


Relation to RefAppState

If you need to store:

  • very large objects

  • mutable graph-like structures

  • non-copyable engine-level data

then SimpleAppState may not be appropriate.

In such cases, see: RefAppState

RefAppState trades safety for performance and uses reference semantics instead of value semantics.


Summary

  • Slots store values, not runtime objects

  • All values must be safely deep-copyable

  • Primitive structures work out of the box

  • Custom classes must extend CloneableFile

  • UI and runtime objects never belong in state

Once these rules are understood, SimpleAppState becomes predictable, safe, and scalable.