Appearance
💾 Local Storage Standards
Overview
Use a three-layer local storage design so feature code stays decoupled from backends and keys are typed and centralized:
StorageServices — Low-level service that talks to SharedPreferences (non-secure) and Flutter Secure Storage (secure). Injected via the DI container; not used directly by feature code.
LocalStorageItem<T> — Typed wrapper for a single storage key. Supports primitives, lists, and JSON-serializable objects, with optional secure storage. Used only via the provider.
LocalStorageProvider — Central registry of all known keys. Exposes static getters that return a
LocalStorageItem<T>for each key (e.g.LocalStorageProvider.token,LocalStorageProvider.user).
Feature code should use only LocalStorageProvider and the LocalStorageItem API (.store(), .get(), .delete()). It must not depend on StorageServices or the backend packages (SharedPreferences, Flutter Secure Storage) directly.
Backends
| Backend | Package | When used |
|---|---|---|
| SharedPreferences | shared_preferences | Non-secure items (secure: false, default) |
| Flutter Secure Storage | flutter_secure_storage | Secure items (secure: true) |
Use SharedPreferences for app settings, preferences, cache, and non-sensitive data. Use Flutter Secure Storage for tokens, credentials, and other sensitive data. On first run, secure storage may be cleared once and a flag set so it is not cleared again; this belongs in the low-level service (e.g. StorageServices), not in feature code.
LocalStorageItem<T>
Supported types
- Primitives:
int,String,bool,double - List:
List<String> - Objects: Any type that implements a serialization contract (e.g.
Serializable), using afromJsoncallback for deserialization
Complex objects are stored as JSON strings. Storing null is equivalent to calling delete().
Constructor
dart
LocalStorageItem(
this.key, {
this.secure = false,
this.fromJson,
})- key — Storage key (string).
- secure — If
true, uses Flutter Secure Storage; otherwise SharedPreferences. - fromJson — Required for non-primitive types:
T Function(Map<String, dynamic>)?. Used when reading to decode JSON toT.
API
| Method | Description |
|---|---|
Future store(T? value) | Writes a value. Passing null removes the key. |
Future<T?> get() | Reads the value, or null if missing/invalid. |
Future<bool> delete() | Removes the key. Returns true on success. |
Feature code does not construct LocalStorageItem directly; it uses the getters from LocalStorageProvider.
LocalStorageProvider
Central registry implemented as an abstract final class with:
- Private
static const String _*Keyfor each key. - Public
static LocalStorageItem<T> get *for each stored value.
Optional helpers (e.g. storeSystemConfig, clearSystemConfig) can batch related keys for convenience.
Core Standards
Storage setup rules
- Use a centralized LocalStorageProvider for all local storage operations; feature code accesses storage only through it.
- Do not use getIt for storage in feature code — use
LocalStorageProvider.*getters andLocalStorageItemmethods. - Choose secure vs non-secure via
LocalStorageItem(..., secure: true)for sensitive data. - Use consistent key naming (e.g. snake_case, private constants in the provider).
- Implement proper error handling in the low-level service; feature code uses the same
.store(),.get(),.delete()API for all keys. - Provide one LocalStorageItem per logical value — each with
.store(),.get(), and.delete().
Key naming conventions
- Use descriptive key names that indicate purpose.
- Use snake_case for key strings.
- Use private static const for key constants in the provider.
- Avoid generic names like
'data'or'value'. - Group related keys (e.g. comment by category: user, app settings, auth).
dart
// ✅ Good: Private constants + descriptive names
static const String _userThemeKey = 'user_theme';
static const String _authTokenKey = 'auth_token';
// ❌ Bad: Generic or public key literals in feature code
static const String _dataKey = 'data';
// and: LocalStorageItem<String>('temp_key') in feature codeHow to add a new stored value
Add a key constant in the LocalStorageProvider (e.g. in
local_storage.dart):dartstatic const String _myKey = 'my_key';Add a getter that returns a
LocalStorageItem<T>:Primitives /
List<String>:dartstatic LocalStorageItem<String> get myValue => LocalStorageItem<String>(_myKey);Secure primitive:
dartstatic LocalStorageItem<String> get mySecret => LocalStorageItem<String>(_mySecretKey, secure: true);Object (with JSON):
dartstatic LocalStorageItem<MyModel> get myModel => LocalStorageItem<MyModel>( _myModelKey, fromJson: MyModel.fromJson, secure: true, // optional );Use it anywhere via the provider:
dartawait LocalStorageProvider.myValue.store('value'); final value = await LocalStorageProvider.myValue.get(); await LocalStorageProvider.myValue.delete();
No getIt or direct use of StorageServices is required in feature code.
Usage examples
dart
// Store and read a string
await LocalStorageProvider.language.store('en');
final lang = await LocalStorageProvider.language.get();
// Store and read secure token
await LocalStorageProvider.token.store('jwt...');
final token = await LocalStorageProvider.token.get();
// Store and read an object (e.g. user)
await LocalStorageProvider.user.store(detailedUser);
final user = await LocalStorageProvider.user.get();
// Remove a value
await LocalStorageProvider.forgotEmail.delete();
// Batch helpers (if defined)
await LocalStorageProvider.storeSystemConfig(systemConfig);
await LocalStorageProvider.clearSystemConfig();File layout
| File | Purpose |
|---|---|
local_storage.dart | LocalStorageProvider and all key/getter definitions (and optional batch helpers) |
local_storage_item.dart | LocalStorageItem<T> implementation |
storage_services.dart | Low-level StorageServices (SharedPreferences + Flutter Secure Storage) |
serializable.dart (or equivalent) | Serialization interface for JSON-serializable models |
Place these under a shared data/local path (e.g. lib/shared/data/local/).
Data type handling
- Primitives and
List<String>: Use the appropriateLocalStorageItem<T>; nofromJsonneeded. - Complex objects: Implement a serialization contract (e.g.
toJson/fromJson) and passfromJsonintoLocalStorageItem. Store as JSON; the item handles encoding/decoding. - Null: Use nullable return types (
T?); treat storingnullas delete. - Defaults: Apply default values at the call site when reading (e.g.
await provider.theme.get() ?? 'light'), not inside the provider.
Common violations
❌ Do not
- Store sensitive data in non-secure storage (SharedPreferences); use
secure: truefor tokens, credentials, PII. - Use raw key strings in feature code; all keys are defined once in the provider.
- Depend on StorageServices or backend packages in feature code; use only LocalStorageProvider and LocalStorageItem.
- Use getIt to access storage in feature code; use LocalStorageProvider getters.
- Use generic key names like
'data'or'value'. - Forget to clear sensitive data on logout (use provider/helpers to delete the relevant keys).
- Store passwords in plain text; use secure storage and secure handling.
- Ignore null safety when retrieving values; use
T?and handle null at the call site.
✅ Always
- Use LocalStorageProvider as the single entry point for feature code.
- Use descriptive, consistent key names and private key constants in the provider.
- Use LocalStorageItem
.store(),.get(), and.delete()for each value. - Set
secure: truefor tokens, credentials, and other sensitive data. - Add new keys via a new constant + getter in the provider; then use the getter everywhere.
- Convert complex objects via JSON and
fromJsonin the item definition. - Clear sensitive data when the user logs out, using provider helpers or explicit deletes.
- Keep StorageServices behind the provider; only the DI container and the provider implementation touch it.