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. .. code-block:: dart final appState = SimpleAppState(); final countSlot = appState.slot('count', initial: 0); final logsSlot = appState.slot>( 'logs', initial: [], caster: (raw) => (raw as List).cast(), ); * **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., ``), 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: .. code-block:: dart caster: (raw) => (raw as List).cast(), 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** instead of **List**). 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. .. code-block:: dart class CounterPage extends SlotStatefulWidget { const CounterPage({super.key}); @override List get slots => [countSlot, logsSlot]; @override SlotState 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()**. .. code-block:: dart // 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. .. code-block:: dart final myData = {'id': 1}; // 1. Initial value is copied. final mySlot = appState.slot('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. .. code-block:: dart 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 :doc:`project_structure` section.