Essentials

Persistent Shard

Learn how to persist state across app sessions with PersistentShard.

Overview

PersistentShard extends Shard<T> and adds automatic state persistence to your shards. It automatically saves your state to local storage and restores it when the app restarts or the shard is recreated.

Why Persistent Shard?

  • Survive App Restarts: Your state persists across app sessions, so users don't lose their data
  • Automatic Management: No manual save/load calls needed — it happens automatically
  • Storage Freedom: Choose any storage backend you want — SharedPreferences, Hive, SQLite, or your custom solution
  • Flexible Configuration: Control when and how state is saved with debouncing, auto-save, and auto-load options

The key advantage is that PersistentShard doesn't lock you into a specific storage solution. You implement the StateStorage interface for your preferred backend, giving you complete freedom to use whatever storage mechanism fits your app's needs.

Creating a Persistent Shard

class TodoShard extends PersistentShard<TodoState> {
  @override
  String get persistenceKey => 'todos';
  
  TodoShard({
    required StateStorage storage,
  }) : super(
        const TodoState(todos: []),
        storage: storage,
        serializer: TodoSerializer(),
        autoSave: true,
        autoLoad: true,
        debounceDuration: const Duration(milliseconds: 500),
      );
  
  void addTodo(String title) {
    emit(state.copyWith(
      todos: [...state.todos, Todo(id: '1', title: title)],
    ));
  }
}

Then create the shard with your storage:

final storage = await SharedPreferencesStorage.create();
final shard = TodoShard(storage: storage);
For async storage initialization, use storageFactory instead. See State Storage section for details.

State Storage

StateStorage is an interface for storage backends. You need to implement it for your storage solution.

Interface

abstract class StateStorage {
  Future<void> save(String key, String value);
  Future<String?> load(String key);
  Future<void> clear();
}

Using Storage (Synchronous)

If you already have a storage instance ready, pass it directly using the storage parameter:

// Create storage instance
final storage = await SharedPreferencesStorage.create();

// Pass to shard
class TodoShard extends PersistentShard<TodoState> {
  TodoShard({required StateStorage storage})
    : super(
        const TodoState(todos: []),
        storage: storage, // Direct storage instance
        serializer: TodoSerializer(),
      );
}

// Usage
final shard = TodoShard(storage: storage);

Using StorageFactory (Asynchronous)

If your storage requires async initialization (like SharedPreferences), use storageFactory:

class TodoShard extends PersistentShard<TodoState> {
  TodoShard()
    : super(
        const TodoState(todos: []),
        storageFactory: () => SharedPreferencesStorage.create(), // Async factory
        serializer: TodoSerializer(),
      );
}

// Usage - storage is initialized automatically
final shard = TodoShard();

When to Use Which?

  • Use storage: When you have the storage instance ready before creating the shard, or when you want to share the same storage instance across multiple shards
  • Use storageFactory: When storage initialization is async (like SharedPreferences, Hive, etc.) and you want the shard to handle initialization automatically

SharedPreferences Implementation Example

import 'package:shared_preferences/shared_preferences.dart';
import 'package:shard/shard.dart';

class SharedPreferencesStorage implements StateStorage {
  final SharedPreferences _prefs;
  
  SharedPreferencesStorage(this._prefs);
  
  static Future<SharedPreferencesStorage> create() async {
    final prefs = await SharedPreferences.getInstance();
    return SharedPreferencesStorage(prefs);
  }
  
  @override
  Future<void> save(String key, String value) async {
    await _prefs.setString(key, value);
  }
  
  @override
  Future<String?> load(String key) async {
    return _prefs.getString(key);
  }
  
  @override
  Future<void> clear() async {
    await _prefs.clear();
  }
}

State Serialization

StateSerializer<T> converts state to/from strings for storage.

Interface

abstract class StateSerializer<T> {
  String serialize(T state);
  T deserialize(String data);
}

JSON Serialization Example

import 'dart:convert';
import 'package:shard/shard.dart';

class TodoSerializer implements StateSerializer<TodoState> {
  @override
  String serialize(TodoState state) {
    return jsonEncode(state.toJson());
  }
  
  @override
  TodoState deserialize(String data) {
    final json = jsonDecode(data) as Map<String, dynamic>;
    return TodoState.fromJson(json);
  }
}

Built-in Serializers

Shard provides built-in serializers for simple types:

// BoolSerializer
final serializer = BoolSerializer();

// IntSerializer
final serializer = IntSerializer();

// DoubleSerializer
final serializer = DoubleSerializer();

Configuration Options

autoSave

Automatically save state when it changes (default: true):

PersistentShard(
  initialState,
  autoSave: true, // Save automatically on state changes
)

autoLoad

Automatically load state when shard is created (default: true):

PersistentShard(
  initialState,
  autoLoad: true, // Load saved state on init
)

debounceDuration

Debounce duration for auto-save (default: 500ms):

PersistentShard(
  initialState,
  debounceDuration: const Duration(milliseconds: 1000), // Save after 1 second of inactivity
)

Manual Control

Save State Manually

final shard = TodoShard();
await shard.saveState();

Load State Manually

final shard = TodoShard();
final loaded = await shard.loadState();
if (loaded) {
  print('State loaded successfully');
}

Clear Storage

final shard = TodoShard();
await shard.clear(); // Clears storage and resets to initial state

Error Handling

PersistentShard provides error callbacks that are automatically called when persistence operations fail. These callbacks also notify observers via addError, ensuring your global error handling is aware of persistence issues.

onLoadError

Called when loading state from storage fails. The shard continues with the initial state, so your app doesn't crash.

Behavior:

  • Shard continues with initialState (the state passed to constructor)
  • Error is automatically reported to observers via addError
  • You can override to add custom handling (analytics, user notifications, etc.)
class TodoShard extends PersistentShard<TodoState> {
  // ... constructor ...
  
  @override
  void onLoadError(Object error, StackTrace? stackTrace) {
    // Always call super to notify observers
    super.onLoadError(error, stackTrace);
    
    // Add your custom error handling
    analytics.logError('load_state_failed', error);
    // or show user notification
  }
}

onSaveError

Called when saving state to storage fails. The in-memory state remains updated, so your app continues normally.

Behavior:

  • In-memory state is already updated (the emit call succeeded)
  • Only the storage write failed
  • Error is automatically reported to observers via addError
  • You can override to add retry logic or user notifications
class TodoShard extends PersistentShard<TodoState> {
  // ... constructor ...
  
  @override
  void onSaveError(Object error, StackTrace? stackTrace) {
    // Always call super to notify observers
    super.onSaveError(error, stackTrace);
    
    // Add your custom error handling
    // Maybe retry the save
    // or show user notification
    showSnackBar('Failed to save. Changes are kept in memory.');
  }
}

Retry Logic

Use retry() to retry loading state:

final shard = TodoShard();
try {
  await shard.loadState();
} catch (e) {
  // Retry after delay
  await Future.delayed(Duration(seconds: 1));
  await shard.retry();
}

StatePersistenceMixin

You can also add persistence to existing shards using the mixin:

class CustomShard extends Shard<MyState> with StatePersistenceMixin<MyState> {
  CustomShard() : super(MyState());
  
  @override
  void onInit() {
    super.onInit();
    enablePersistence(
      key: 'my_state',
      storage: await SharedPreferencesStorage.create(),
      serializer: MyStateSerializer(),
    );
  }
}

Next Steps

Now that you understand persistence, learn about Widgets to integrate shards with Flutter widgets, or explore Debounce & Throttle to optimize state updates.