Common Mistakes

This section lists common mistakes made by new users of SimpleAppState. Each mistake is explained together with the correct mental model.


Treating state as widget-owned

Mistake

Defining SimpleAppState or slots inside widgets:

class MyWidget extends StatelessWidget {
  final appState = SimpleAppState(); // ❌
}

Why this is a problem

Widgets in Flutter are ephemeral. They can be rebuilt, recreated, or disposed at any time.

Placing application state inside widgets makes state lifetime hard to reason about and breaks persistence and undo/redo use cases.

Correct approach

Define application state at a higher level, typically as a global or top-level object:

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

Widgets should subscribe to state, not own it.


Mutating values returned from slots

Mistake

Assuming that modifying a value returned from get() will update application state:

final list = logs.get();
list.add('new entry'); // ❌ has no effect on the original state

Why this is a problem

Values returned from StateSlot.get() are always deep-copied. This is a safety feature to prevent accidental side effects. Direct mutation of a value obtained via get() never affects the internal state of the slot, and therefore won’t trigger UI rebuilds.

Correct approach

Always use set() or update().

Crucially, in SimpleAppState, the old Value passed to the update closure is already a deep-copy. This means you can safely mutate it directly and return it. You don’t need to manually create a new List or Map instance.

// Correct and safe:
// The 'oldCopy' list is already a copy, so you can mutate it directly.
logs.update((oldCopy) {
  oldCopy.add('new entry');
  return oldCopy;
});

// Also works for custom objects:
userSlot.update((user) {
  user.name = 'New Name';
  return user;
});

SimpleAppState ensures that these mutations remain local to the update process and do not leak into other parts of your application, keeping your code clean and predictable.


Forgetting to declare slot dependencies

Mistake

Accessing slot values in a widget without declaring them in slots:

class CounterView extends SlotStatefulWidget {
  @override
  List<StateSlot> get slots => []; // ❌
}

Why this is a problem

SimpleAppState does not use implicit dependency tracking. If a widget does not declare a slot, it will not rebuild when that slot changes.

Correct approach

Explicitly list all slots the widget depends on:

@override
List<StateSlot> get slots => [count];

Creating many small state objects

Mistake

Creating multiple SimpleAppState instances for logically related application state.

Why this is a problem

Splitting state ownership too early makes it difficult to perform coordinated updates and persistence.

Correct approach

Start with a single SimpleAppState instance. Introduce multiple state objects only when you have clear ownership boundaries.

A specific case where multiple SimpleAppStates instance are needed is when you want to separate the app state that is the target of Undo and Redo from the rest.


Mixing Import Types

Mistake

Mixing relative imports and absolute package imports for the same file:

// In main.dart
import 'package:my_app/ui/app_state.dart';

// In counter_page.dart
import '../app_state.dart'; // ❌ Mixing with relative import

Why this is a problem

Dart treats absolute and relative imports as different libraries. This means the appState object in main.dart will be a different instance from the one in counter_page.dart.

If you update the state in one file, the UI listening in the other file will not reflect the change, leading to extremely hard-to-debug issues.

Correct approach

Always use absolute package imports for any file that defines or uses your application state:

import 'package:my_app/ui/app_state.dart';

Storing secrets in slots

Mistake

Storing confidential or security-sensitive data in StateSlots, or using such data as slot names:

// ❌ Bad: secrets should not be in SimpleAppState at all
final password = appState.slot<String>('user_password', initial: 'hunter2');
final apiKey   = appState.slot<String>('api_key', initial: 'sk-live-abcdef');

Why this is a problem

Slots in SimpleAppState are designed for application state:

  • They have mandatory initial values

  • They participate in persistence and restore

  • They are tracked by undo/redo

  • They can be inspected by debug listeners

  • They may appear in logs, errors, and crash reports

This means any value stored in a slot must be assumed to be:

  • Serializable

  • Debuggable

  • Potentially observable

Secrets such as passwords, API keys, tokens, or private credentials violate these assumptions. Even if debug tools are disabled in production, storing secrets in state objects that are designed for persistence and inspection is fundamentally unsafe.

Correct approach

Do not store secrets in SimpleAppState at all.

Use a dedicated secure storage mechanism instead, such as:

  • Platform keychains / keystores

  • Secure enclaves

  • OS-provided credential APIs

  • Encrypted storage services

Slots should only contain data that is safe to:

  • Persist

  • Log

  • Restore

  • Debug

If you need to reflect authentication or authorization status in your UI, store only non-sensitive derived state in slots:

// ✅ Safe: derived, non-secret state
final isLoggedIn = appState.slot<bool>('is_logged_in', initial: false);
final userId    = appState.slot<String?>('user_id', initial: null);

Accessing slots from utility functions

Mistake

Accessing StateSlot or SimpleAppState directly from utility or helper functions:

// util.dart
static void addLog(String message) {
  logs.update((oldCopy) {
    oldCopy.add(message);
    return oldCopy;
  });
}

Why this is a problem

Utility functions are, by definition, stateless. They should not own or mutate application state.

When utilities access slots directly:

  • State mutations become hidden and hard to trace

  • Call sites no longer reveal which state is affected

  • Testing and reasoning about state flow becomes difficult

This breaks one of the core principles of SimpleAppState: all state access must be explicit and visible.

Correct approach

Pass data into utilities, and return new values out. State updates should happen at the call site:

// util.dart
static List<String> addLog(List<String> oldCopy, String message) {
  oldCopy.add(message);
  return oldCopy;
}

// call site
logs.update((oldCopy) => addLog(oldCopy, message));

Utilities transform values. StateSlots own state.


Expecting context-based lookups

Mistake

Assuming that widgets can implicitly access application state.

Why this is a problem

SimpleAppState intentionally avoids implicit or context-based state access. All state dependencies must be explicit and visible in code.

This keeps rebuild behavior predictable and makes it clear which state changes can affect a given widget.

Correct approach

Pass or import the required StateSlot explicitly. If a widget rebuilds, you should always be able to point to the exact slot that caused it.


Updating state during build

Mistake

Updating application state during widget build:

@override
Widget build(BuildContext context) {
  countSlot.update((v) => v + 1); // ❌
  return const Text('Hello');
}

Why this is a problem

Widget build methods must be pure. They may run many times, and changing state during build can cause repeated rebuilds or unstable behavior.

Correct approach

Only update state in response to events (such as user actions or lifecycle callbacks).

For a detailed explanation, see: Flutter Common Mistakes


Next step

Congratulations! You’ve mastered the basics of SimpleAppState.

From here, you can explore more advanced topics depending on your interests:

Happy development!