Skip to content

💾 Local Storage Standards

Table of Contents

Core Standards

Storage Setup Rules

  1. Always use centralized LocalStorage class for all local storage operations
  2. Use dependency injection with getIt for access
  3. Choose appropriate storage type based on data sensitivity
  4. Use consistent key naming conventions
  5. Implement proper error handling for storage operations
  6. Provide getter, setter, and clear methods for each stored value

Storage Type Selection Rules

  1. Use SharedPreferences for non-sensitive data (settings, preferences, cache)
  2. Use FlutterSecureStorage for sensitive data (tokens, passwords, personal info)
  3. Consider data persistence requirements
  4. Evaluate performance needs for frequent access
  5. Check platform compatibility for secure storage
dart
// Non-sensitive data - SharedPreferences
static const String _themeKey = 'app_theme';
static const String _languageKey = 'app_language';
static const String _lastSyncKey = 'last_sync_time';

// Sensitive data - FlutterSecureStorage
static const String _authTokenKey = 'auth_token';
static const String _userPasswordKey = 'user_password';
static const String _biometricKey = 'biometric_enabled';

Storage Types

SharedPreferences Usage

  1. Use for app settings and user preferences
  2. Use for cached data that's not sensitive
  3. Use for UI state persistence
  4. Use for analytics data and app metrics
  5. Handle null values properly
dart
// String values
Future<bool> storeTheme(String theme) async =>
    _sharedPreferences.setString(_themeKey, theme);

String? get theme => _sharedPreferences.getString(_themeKey);

Future<bool> clearTheme() async => _sharedPreferences.remove(_themeKey);

// Boolean values
Future<bool> storeNotificationsEnabled(bool enabled) async =>
    _sharedPreferences.setBool('notifications_enabled', enabled);

bool get notificationsEnabled => 
    _sharedPreferences.getBool('notifications_enabled') ?? true;

// Integer values
Future<bool> storeUserId(int userId) async =>
    _sharedPreferences.setInt('user_id', userId);

int? get userId => _sharedPreferences.getInt('user_id');

// List values
Future<bool> storeRecentSearches(List<String> searches) async =>
    _sharedPreferences.setStringList('recent_searches', searches);

List<String> get recentSearches => 
    _sharedPreferences.getStringList('recent_searches') ?? [];

FlutterSecureStorage Usage

  1. Use for authentication tokens and credentials
  2. Use for personal information and sensitive data
  3. Handle secure storage errors gracefully
dart
// Authentication tokens
Future<void> storeAuthToken(String token) async =>
    await _secureStorage.write(key: _authTokenKey, value: token);

Future<String?> get authToken async =>
    await _secureStorage.read(key: _authTokenKey);

Future<void> clearAuthToken() async =>
    await _secureStorage.delete(key: _authTokenKey);

// User credentials
Future<void> storeUserCredentials(String username, String password) async {
  await _secureStorage.write(key: 'username', value: username);
  await _secureStorage.write(key: 'password', value: password);
}

Future<Map<String, String?>> get userCredentials async => {
  'username': await _secureStorage.read(key: 'username'),
  'password': await _secureStorage.read(key: 'password'),
};

// Clear all secure storage
Future<void> clearAllSecureData() async =>
    await _secureStorage.deleteAll();

Implementation Guidelines

Key Naming Conventions

  1. Use descriptive key names that indicate purpose
  2. Use snake_case for key naming
  3. Use consistent prefixes for related keys
  4. Avoid generic names like 'data' or 'value'
  5. Include data type in key name when helpful
dart
// ✅ Good: Descriptive and consistent naming
static const String _userThemeKey = 'user_theme';
static const String _lastLoginTimeKey = 'last_login_time';
static const String _notificationSettingsKey = 'notification_settings';
static const String _authRefreshTokenKey = 'auth_refresh_token';

// ❌ Bad: Generic and unclear naming
static const String _dataKey = 'data';
static const String _valueKey = 'value';
static const String _key1 = 'key1';
static const String _tempKey = 'temp';

Method Implementation Rules

  1. Always provide getter, setter, and clear methods for each stored value
  2. Use appropriate return types for getters
  3. Handle null values with default values or nullable types
  4. Use async/await for all storage operations
  5. Implement proper error handling
dart
class LocalStorage {
  // Theme management
  static const String _themeKey = 'app_theme';

  Future<bool> storeTheme(String theme) async {
    try {
      return await _sharedPreferences.setString(_themeKey, theme);
    } catch (e) {
      // Log error and return false
      return false;
    }
  }

  String get theme => _sharedPreferences.getString(_themeKey) ?? 'light';

  Future<bool> clearTheme() async {
    try {
      return await _sharedPreferences.remove(_themeKey);
    } catch (e) {
      // Log error and return false
      return false;
    }
  }

  // User preferences
  static const String _notificationsEnabledKey = 'notifications_enabled';

  Future<bool> storeNotificationsEnabled(bool enabled) async {
    try {
      return await _sharedPreferences.setBool(_notificationsEnabledKey, enabled);
    } catch (e) {
      return false;
    }
  }

  bool get notificationsEnabled => 
      _sharedPreferences.getBool(_notificationsEnabledKey) ?? true;

  Future<bool> clearNotificationsEnabled() async {
    try {
      return await _sharedPreferences.remove(_notificationsEnabledKey);
    } catch (e) {
      return false;
    }
  }
}

Data Type Handling Rules

  1. Use appropriate data types for different values
  2. Convert complex objects to JSON strings
  3. Handle type conversion errors gracefully
  4. Use nullable types when values might not exist
  5. Provide default values for required settings
dart
// Complex object storage
Future<bool> storeUserSettings(UserSettings settings) async {
  try {
    final jsonString = jsonEncode(settings.toJson());
    return await _sharedPreferences.setString('user_settings', jsonString);
  } catch (e) {
    return false;
  }
}

UserSettings? get userSettings {
  try {
    final jsonString = _sharedPreferences.getString('user_settings');
    if (jsonString != null) {
      final json = jsonDecode(jsonString) as Map<String, dynamic>;
      return UserSettings.fromJson(json);
    }
    return null;
  } catch (e) {
    return null;
  }
}

// Date storage
Future<bool> storeLastSyncTime(DateTime dateTime) async {
  try {
    return await _sharedPreferences.setString(
      'last_sync_time', 
      dateTime.toIso8601String(),
    );
  } catch (e) {
    return false;
  }
}

DateTime? get lastSyncTime {
  try {
    final dateString = _sharedPreferences.getString('last_sync_time');
    if (dateString != null) {
      return DateTime.parse(dateString);
    }
    return null;
  } catch (e) {
    return null;
  }
}

Key Management

Key Organization Rules

  1. Group related keys together in the class
  2. Use static const for all key definitions
  3. Document key purposes with comments
  4. Avoid key conflicts with consistent naming
  5. Use versioning for key changes when needed
dart
class LocalStorage {
  // User-related keys
  static const String _userIdKey = 'user_id';
  static const String _userEmailKey = 'user_email';
  static const String _userPreferencesKey = 'user_preferences';

  // App settings keys
  static const String _themeKey = 'app_theme';
  static const String _languageKey = 'app_language';
  static const String _firstLaunchKey = 'first_launch';

  // Authentication keys (secure storage)
  static const String _authTokenKey = 'auth_token';
  static const String _refreshTokenKey = 'refresh_token';
  static const String _biometricEnabledKey = 'biometric_enabled';

  // Cache keys
  static const String _lastCacheUpdateKey = 'last_cache_update';
  static const String _cachedDataVersionKey = 'cached_data_version';
}

Access Pattern Rules

  1. Use dependency injection for LocalStorage access
  2. Access through getIt from anywhere in the app
  3. Handle storage errors at the access point
  4. Use consistent access patterns across the app
  5. Provide fallback values when storage fails
dart
// Access pattern example
class UserService {
  final LocalStorage _localStorage = GetIt.instance<LocalStorage>();

  Future<void> saveUserPreferences(UserPreferences preferences) async {
    final success = await _localStorage.storeUserPreferences(preferences);
    if (!success) {
      // Handle storage failure
      throw Exception('Failed to save user preferences');
    }
  }

  UserPreferences getUserPreferences() {
    return _localStorage.userPreferences ?? UserPreferences.defaults();
  }

  Future<void> clearUserData() async {
    await _localStorage.clearUserPreferences();
    await _localStorage.clearUserId();
  }
}

Common Violations

DO NOT Violate These Rules

  1. Don't store sensitive data in SharedPreferences
  2. Don't use hardcoded keys without constants
  3. Don't ignore storage errors - always handle them
  4. Don't use generic key names like 'data' or 'value'
  5. Don't store large objects directly in SharedPreferences
  6. Don't forget to clear sensitive data on logout
  7. Don't use inconsistent naming conventions for keys
  8. Don't store passwords in plain text
  9. Don't ignore null safety when retrieving values
  10. Don't forget to register LocalStorage with dependency injection

ALWAYS Follow These Rules

  1. Use appropriate storage type based on data sensitivity
  2. Use descriptive key names with consistent naming
  3. Provide getter, setter, and clear methods for each value
  4. Handle storage errors gracefully with try-catch
  5. Use dependency injection for LocalStorage access
  6. Convert complex objects to JSON before storage
  7. Provide default values for required settings
  8. Clear sensitive data when user logs out
  9. Use static const for all key definitions
  10. Test storage operations in different scenarios