Understanding the Basics

Now that you have seen the complete code, let’s break down the three core concepts of SimpleAppState.

Even though the previous example was in a single file, it followed a strict separation of concerns. Understanding these patterns is key to building scalable Flutter apps.


1. State Definition: The “Source of Truth”

In SimpleAppState, application state lives outside the widget tree. This makes the state independent of the UI lifecycle.

final appState = SimpleAppState();
final countSlot = appState.slot<int>('count', initial: 0);
final logsSlot = appState.slot<List<String>>(
  'logs',
  initial: [],
  caster: (raw) => (raw as List).cast<String>(),
);
  • SimpleAppState instance (appState): A central container for all your data.

  • StateSlot (countSlot, logsSlot): A specific “drawer” in that container. Each slot:
    • Has a unique name (e.g., ‘count’) for debugging and persistence.

    • Has a strict type (e.g., <int>), ensuring type safety throughout the app.

    • Is globally accessible, so any widget can find it without passing data through constructors.

Note: Why do we need a caster?

You might notice the caster property in the logsSlot definition:

caster: (raw) => (raw as List).cast<String>(),

SimpleAppState is designed to keep your data safe by performing “deep copies” (creating a fresh duplicate of your data). However, because of how Dart works, when a List or Map is copied, it sometimes “loses” its specific type information (e.g., it becomes a generic List<dynamic> instead of List<String>).

The caster is a simple helper function that:

  • Ensures the data maintains the correct type after being copied.

  • Acts as a safety guard to make sure only the correct type of data enters the slot.

For simple types like int or String, you don’t need a caster. But for Lists and Maps, always include it to keep your code type-safe and prevent errors.


2. Reactive UI: How Widgets “Listen”

Widgets do not “own” application state; they subscribe to it. When a slot’s value changes, only the widgets listening to that specific slot will rebuild.

class CounterPage extends SlotStatefulWidget {
  const CounterPage({super.key});

  @override
  List<StateSlot> get slots => [countSlot, logsSlot];

  @override
  SlotState<CounterPage> createState() => _CounterPageState();
}
  • Minimal Rebuilds: If you have 10 slots but only subscribe to countSlot, changes in other slots will not trigger a rebuild of this widget. This keeps your app performant.


3. Logic: Updating State Atomically

To change the UI, you change the state. SimpleAppState provides two ways to modify slots: set() and update().

// Direct update
countSlot.set(10);

// Update based on the previous value
countSlot.update((oldCopy) => oldCopy + 1);

The Triple-Layer Safety: Total Isolation

SimpleAppState ensures that your application state is a “Fortress.” Once data is handled by a slot, it is completely isolated from the rest of your code’s memory. This is achieved through three layers of protection:

  1. Protection on Definition (initial): Even the initial value you provide is deep-copied immediately. If you change the original object after defining the slot, the state remains safe.

  2. Protection on Exit (get / update): When you retrieve a value, you get a fresh deep copy. Mutating this copy will never accidentally change the “source of truth” inside the slot.

  3. Protection on Entry (set / update): When you save a new value, SimpleAppState performs another deep copy before storing it. This prevents the state from being affected by any future changes to the variable you just passed in.

final myData = {'id': 1};
// 1. Initial value is copied.
final mySlot = appState.slot<Map>('mySlot', initial: myData);

myData['id'] = 999; // ❌ This has NO effect on the slot.

print(mySlot.get()['id']); // Always 1

Batching Updates

When you need to update multiple slots at once, use appState.batch(). This is useful when several changes represent a single logical action.

appState.batch(() {
  countSlot.update((v) => v + 1);
  logsSlot.update((oldCopy) {
    oldCopy.add('Increment at ${DateTime.now()}');
    return oldCopy;
  });
});
  • Sequential but Atomic: Changes are applied inside the batch one by one. If you call get() inside the block, you will receive the updated value immediately.

  • Efficient UI Notification: SimpleAppState waits until the entire block is finished before notifying the UI.

  • No Intermediate States: Even if you update multiple slots, the UI only rebuilds once (More precisely, a single screen update is requested). This prevents the user from seeing inconsistent “half-updated” data and improves performance.


Summary of the Pattern

The lifecycle of a SimpleAppState app always follows this loop:

  1. Define: Create a StateSlot in a global location.

  2. Display: Use SlotStatefulWidget to show the value in the UI.

  3. Act: Call update() or set() from a button or event.

  4. React: The UI automatically updates to reflect the new state.


Next step

Now that you understand the “How” and “Why” of the code, let’s look at how to organize these pieces into a professional directory structure.

Go to Scaling to a Real Project Structure section.