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!