Skip to content

💾 Local Storage Standards

Overview

Use a three-layer local storage design so feature code stays decoupled from backends and keys are typed and centralized:

  1. 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.

  2. 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.

  3. 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

BackendPackageWhen used
SharedPreferencesshared_preferencesNon-secure items (secure: false, default)
Flutter Secure Storageflutter_secure_storageSecure 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 a fromJson callback 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 to T.

API

MethodDescription
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 _*Key for 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

  1. Use a centralized LocalStorageProvider for all local storage operations; feature code accesses storage only through it.
  2. Do not use getIt for storage in feature code — use LocalStorageProvider.* getters and LocalStorageItem methods.
  3. Choose secure vs non-secure via LocalStorageItem(..., secure: true) for sensitive data.
  4. Use consistent key naming (e.g. snake_case, private constants in the provider).
  5. Implement proper error handling in the low-level service; feature code uses the same .store(), .get(), .delete() API for all keys.
  6. Provide one LocalStorageItem per logical value — each with .store(), .get(), and .delete().

Key naming conventions

  1. Use descriptive key names that indicate purpose.
  2. Use snake_case for key strings.
  3. Use private static const for key constants in the provider.
  4. Avoid generic names like 'data' or 'value'.
  5. 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 code

How to add a new stored value

  1. Add a key constant in the LocalStorageProvider (e.g. in local_storage.dart):

    dart
    static const String _myKey = 'my_key';
  2. Add a getter that returns a LocalStorageItem<T>:

    Primitives / List<String>:

    dart
    static LocalStorageItem<String> get myValue =>
        LocalStorageItem<String>(_myKey);

    Secure primitive:

    dart
    static LocalStorageItem<String> get mySecret =>
        LocalStorageItem<String>(_mySecretKey, secure: true);

    Object (with JSON):

    dart
    static LocalStorageItem<MyModel> get myModel =>
        LocalStorageItem<MyModel>(
          _myModelKey,
          fromJson: MyModel.fromJson,
          secure: true,  // optional
        );
  3. Use it anywhere via the provider:

    dart
    await 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

FilePurpose
local_storage.dartLocalStorageProvider and all key/getter definitions (and optional batch helpers)
local_storage_item.dartLocalStorageItem<T> implementation
storage_services.dartLow-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 appropriate LocalStorageItem<T>; no fromJson needed.
  • Complex objects: Implement a serialization contract (e.g. toJson / fromJson) and pass fromJson into LocalStorageItem. Store as JSON; the item handles encoding/decoding.
  • Null: Use nullable return types (T?); treat storing null as 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

  1. Store sensitive data in non-secure storage (SharedPreferences); use secure: true for tokens, credentials, PII.
  2. Use raw key strings in feature code; all keys are defined once in the provider.
  3. Depend on StorageServices or backend packages in feature code; use only LocalStorageProvider and LocalStorageItem.
  4. Use getIt to access storage in feature code; use LocalStorageProvider getters.
  5. Use generic key names like 'data' or 'value'.
  6. Forget to clear sensitive data on logout (use provider/helpers to delete the relevant keys).
  7. Store passwords in plain text; use secure storage and secure handling.
  8. Ignore null safety when retrieving values; use T? and handle null at the call site.

✅ Always

  1. Use LocalStorageProvider as the single entry point for feature code.
  2. Use descriptive, consistent key names and private key constants in the provider.
  3. Use LocalStorageItem .store(), .get(), and .delete() for each value.
  4. Set secure: true for tokens, credentials, and other sensitive data.
  5. Add new keys via a new constant + getter in the provider; then use the getter everywhere.
  6. Convert complex objects via JSON and fromJson in the item definition.
  7. Clear sensitive data when the user logs out, using provider helpers or explicit deletes.
  8. Keep StorageServices behind the provider; only the DI container and the provider implementation touch it.