DEV Community

Cover image for Building a Robust Local Storage Service in Flutter
Abdur Rafay Saleem
Abdur Rafay Saleem

Posted on

Building a Robust Local Storage Service in Flutter

Modern mobile applications often need to store various types of data locally - from user preferences to authentication tokens. While Flutter provides SharedPreferences for basic storage and FlutterSecureStorage for encrypted storage, managing these effectively in a large application requires careful architectural planning.

In this article, we'll explore how to build a robust key-value storage system that separates concerns, provides type safety, and makes storage operations maintainable and secure.

The Two-Layer Architecture

Our implementation uses a two-layer architecture:

  1. KeyValueStorageBase: A low-level base class that directly interfaces with storage plugins
  2. KeyValueStorageService: A high-level service that provides typed, domain-specific storage operations

Why Two Layers?

This separation serves several important purposes:

  1. Separation of Concerns

    • Base class is more generic and handles raw storage operations
    • Service class handles business logic and data transformation according to the app domain
    • Clear boundary between storage mechanism and business logic
  2. Single Responsibility

    • Base class: How to store and retrieve data
    • Service class: What to store and when as well as managing the keys.
  3. Dependency Isolation

    • Only the base class knows about SharedPreferences and FlutterSecureStorage
    • Application code only interacts with the service layer

The Base Layer: KeyValueStorageBase

Let's look at our base class key_value_storage_base.dart implementation:

Initialization

WidgetsBinding.ensureInitialized();
...
// For preparing the key-value mem cache
await KeyValueStorageBase.init();
...
runApp();
Enter fullscreen mode Exit fullscreen mode

The init() method is a crucial part of our base layer design. By initializing both storage systems (SharedPreferences and FlutterSecureStorage) at app startup, we ensure that the storage instances are ready before any part of the app tries to access them. It allows us to perform synchronous reads from SharedPreferences throughout our app's lifecycle.

Without this initialization pattern, we'd need to handle async operations for every read operation, which would significantly complicate our storage API and force unnecessary complexity onto the rest of our application code. This "initialize once, use synchronously" pattern is particularly valuable for accessing critical data like user authentication state or app configuration during app startup.

Key Features of the Base Layer

  1. Unified Storage Interface
   T? getCommon<T>(String key)
   Future<String?> getEncrypted(String key)
   Future<bool> setCommon<T>(String key, T value)
   Future<bool> setEncrypted(String key, String value)
Enter fullscreen mode Exit fullscreen mode
  1. Type-Safe Operations
   T? getCommon<T>(String key) {
     return switch (T) {
       const (String) => _sharedPrefs!.getString(key) as T?,
       const (List<String>) => _sharedPrefs!.getStringList(key) as T?,
       const (int) => _sharedPrefs!.getInt(key) as T?,
       const (bool) => _sharedPrefs!.getBool(key) as T?,
       const (double) => _sharedPrefs!.getDouble(key) as T?,
       _ => _sharedPrefs!.get(key) as T?
     };
   }
Enter fullscreen mode Exit fullscreen mode
  1. Error Handling
   try {
     return _secureStorage!.read(key: key);
   } on PlatformException catch (ex) {
     appLogger.debug('$ex');
     return Future<String?>.value();
   }
Enter fullscreen mode Exit fullscreen mode

The Service Layer: KeyValueStorageService

The service layer provides domain-specific storage operations:

Key Features of the Service Layer

  1. Domain-Specific Operations

    • Methods map directly to business needs
    • Handles serialization/deserialization
    • Provides type safety at the domain level
  2. Centralized Key Management

   static const _authTokenKey = 'authToken';
   static const _authUserKey = 'authUserKey';
Enter fullscreen mode Exit fullscreen mode
  1. Intelligent Storage Decisions

    • Sensitive data uses encrypted storage
    • Regular data uses shared preferences
  2. Bulk Operations

   void clearUserData() {
     _keyValueStorage
       ..removeCommon(_authUserKey)
       ..removeCommon(_firstMapLoadKey)
       ..removeEncrypted(_authTokenKey);
   }
Enter fullscreen mode Exit fullscreen mode

Benefits of This Architecture

  1. Type Safety

    • Compile-time type checking for stored values
    • Domain models are properly serialized/deserialized
  2. Security

    • Clear separation between secure and non-secure storage
    • Encrypted storage for sensitive data
    • Centralized security decisions
  3. Maintainability

    • Single source of truth for storage operations
    • Easy to add new storage operations
    • Consistent error handling
  4. Testability

    • Base class can be mocked for testing
    • Service layer can be tested independently
    • Clear boundaries for unit tests
  5. Performance

    • Async operations where needed
    • Synchronous operations where possible
    • Background execution for non-critical writes

Usage in Application Code

The service is easily accessible through a Riverpod provider:

final keyValueStorageServiceProvider = Provider((ref) {
  return KeyValueStorageService();
});

// In application code
final user = ref.read(keyValueStorageServiceProvider).getAuthUser();
Enter fullscreen mode Exit fullscreen mode

However, you don't have to use riverpod and you can easily change it to some other DI service or simply make it a singleton pattern as well.

Conclusion

This two-layer architecture provides a robust foundation for key-value storage in Flutter applications. By following this pattern you create a clean, maintainable, and secure storage system that can grow with your application.

Credit

If you like it, please also take a look at the Analytics Service article I wrote to see it in action.

Finally, please keep following me as I will be posting about many more advanced stuff like PermissionService, LocationService, and NotificationService etc.

Top comments (0)