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.
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.
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);
storageFactory instead. See State Storage section for details.StateStorage is an interface for storage backends. You need to implement it for your storage solution.
abstract class StateStorage {
Future<void> save(String key, String value);
Future<String?> load(String key);
Future<void> clear();
}
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);
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();
storage: When you have the storage instance ready before creating the shard, or when you want to share the same storage instance across multiple shardsstorageFactory: When storage initialization is async (like SharedPreferences, Hive, etc.) and you want the shard to handle initialization automaticallyimport '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();
}
}
StateSerializer<T> converts state to/from strings for storage.
abstract class StateSerializer<T> {
String serialize(T state);
T deserialize(String data);
}
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);
}
}
Shard provides built-in serializers for simple types:
// BoolSerializer
final serializer = BoolSerializer();
// IntSerializer
final serializer = IntSerializer();
// DoubleSerializer
final serializer = DoubleSerializer();
Automatically save state when it changes (default: true):
PersistentShard(
initialState,
autoSave: true, // Save automatically on state changes
)
Automatically load state when shard is created (default: true):
PersistentShard(
initialState,
autoLoad: true, // Load saved state on init
)
Debounce duration for auto-save (default: 500ms):
PersistentShard(
initialState,
debounceDuration: const Duration(milliseconds: 1000), // Save after 1 second of inactivity
)
final shard = TodoShard();
await shard.saveState();
final shard = TodoShard();
final loaded = await shard.loadState();
if (loaded) {
print('State loaded successfully');
}
final shard = TodoShard();
await shard.clear(); // Clears storage and resets to initial state
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.
Called when loading state from storage fails. The shard continues with the initial state, so your app doesn't crash.
Behavior:
initialState (the state passed to constructor)addErrorclass 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
}
}
Called when saving state to storage fails. The in-memory state remains updated, so your app continues normally.
Behavior:
emit call succeeded)addErrorclass 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.');
}
}
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();
}
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(),
);
}
}
Now that you understand persistence, learn about Widgets to integrate shards with Flutter widgets, or explore Debounce & Throttle to optimize state updates.