saveable 0.1.0
saveable: ^0.1.0 copied to clipboard
Automatic variable-level state persistence.
Saveable #
Automatic variable-level state persistence for Flutter.
Like HydratedBloc, but for individual values instead of entire states. Saveable automatically persists values when they change and restores them when your app starts.
Features #
- Auto-hydration - Automatically loads the last saved value on creation
- Auto-persistence - Automatically saves when the value changes
- Debounced writes - Prevents excessive storage writes during rapid updates
- Type-safe - Full generic support with optional JSON serialization
- UI-ready - Implements
ValueListenablefor easy widget binding - Storage agnostic - Bring your own storage backend (HydratedBloc, Hive, SharedPreferences, etc.)
- Lifecycle management - Built-in disposal and cleanup via
StateSavermixin
Installation #
Add saveable to your pubspec.yaml:
dependencies:
saveable: ^0.1.0
Then run:
flutter pub get
Quick Start #
1. Implement Storage #
First, implement the Storage interface with your preferred backend:
import 'dart:convert';
import 'package:saveable/saveable.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SharedPreferencesStorage implements Storage {
final SharedPreferences _prefs;
SharedPreferencesStorage(this._prefs);
@override
dynamic read(String key) {
final value = _prefs.getString(key);
if (value == null) return null;
return jsonDecode(value);
}
@override
Future<void> write(String key, dynamic value) async {
await _prefs.setString(key, jsonEncode(value));
}
@override
Future<void> delete(String key) async {
await _prefs.remove(key);
}
@override
Future<void> clear() async {
await _prefs.clear();
}
@override
Future<void> close() async {
// SharedPreferences doesn't need to be closed
}
}
2. Use Saveable #
Simple Types
For primitives like int, String, bool, double:
final counter = Saveable<int>(
key: 'counter',
storage: myStorage,
defaultValue: 0,
);
// Read
print(counter.value); // 0
// Write (auto-persists)
counter.value = 42;
// Update with function
counter.update((current) => current + 1);
Complex Types
For custom objects, provide fromJson and toJson:
final user = Saveable<User>(
key: 'user',
storage: myStorage,
defaultValue: User.empty(),
fromJson: User.fromJson,
toJson: (u) => u.toJson(),
);
// Update with copyWith pattern
user.update((current) => current.copyWith(name: 'John'));
3. Use StateSaver Mixin #
The StateSaver mixin simplifies creating multiple saveables with shared configuration:
class SettingsProvider with StateSaver {
late final Saveable<bool> darkMode;
late final Saveable<String> locale;
late final Saveable<UserPreferences> preferences;
@override
Storage get storage => myStorage;
@override
String get storagePrefix => 'settings';
SettingsProvider() {
// Simple types
darkMode = saveable<bool>(
key: 'dark_mode',
defaultValue: false,
);
locale = saveable<String>(
key: 'locale',
defaultValue: 'en',
);
// Complex types
preferences = saveable<UserPreferences>(
key: 'preferences',
defaultValue: UserPreferences.defaults(),
fromJson: UserPreferences.fromJson,
toJson: (p) => p.toJson(),
);
}
void dispose() {
disposeSaveables(); // Cleans up all saveables
}
}
4. Bind to UI #
Saveable implements ValueListenable, making it easy to use with Flutter widgets:
ValueListenableBuilder<bool>(
valueListenable: settings.darkMode,
builder: (context, isDark, child) {
return Switch(
value: isDark,
onChanged: (value) => settings.darkMode.value = value,
);
},
);
Storage Implementations #
Using with HydratedBloc #
If you're already using hydrated_bloc, you can wrap its storage:
import 'package:hydrated_bloc/hydrated_bloc.dart' as hydrated;
import 'package:saveable/saveable.dart';
class HydratedBlocStorage implements Storage {
final hydrated.Storage _storage;
HydratedBlocStorage(this._storage);
@override
dynamic read(String key) => _storage.read(key);
@override
Future<void> write(String key, dynamic value) => _storage.write(key, value);
@override
Future<void> delete(String key) => _storage.delete(key);
@override
Future<void> clear() => _storage.clear();
@override
Future<void> close() => _storage.close();
}
// Usage
final storage = HydratedBlocStorage(HydratedBloc.storage);
Using with Hive #
import 'package:hive/hive.dart';
import 'package:saveable/saveable.dart';
class HiveStorage implements Storage {
final Box _box;
HiveStorage(this._box);
@override
dynamic read(String key) => _box.get(key);
@override
Future<void> write(String key, dynamic value) => _box.put(key, value);
@override
Future<void> delete(String key) => _box.delete(key);
@override
Future<void> clear() => _box.clear();
@override
Future<void> close() => _box.close();
}
// Usage
final box = await Hive.openBox('saveable');
final storage = HiveStorage(box);
API Reference #
Saveable #
| Property/Method | Description |
|---|---|
value |
Get or set the current value |
isHydrated |
Whether the value has been loaded from storage |
key |
The storage key |
update(fn) |
Update value using a function |
clear() |
Remove from storage (keeps in-memory value) |
reset(defaultValue) |
Clear storage and reset to default |
persistNow() |
Force immediate persist (bypasses debounce) |
dispose() |
Clean up resources |
StateSaver Mixin #
| Property/Method | Description |
|---|---|
storage |
Override to provide storage implementation |
storagePrefix |
Optional prefix for all keys |
saveable<T>(...) |
Factory to create tracked Saveable instances |
disposeSaveables() |
Dispose all created saveables |
clearAllSaveables() |
Clear all from storage |
persistAllNow() |
Force persist all saveables immediately |
Storage Interface #
abstract class Storage {
dynamic read(String key);
Future<void> write(String key, dynamic value);
Future<void> delete(String key);
Future<void> clear();
Future<void> close();
}
Configuration #
Debounce Time #
Control how long to wait before persisting after a value change:
final counter = Saveable<int>(
key: 'counter',
storage: myStorage,
defaultValue: 0,
debounceTime: const Duration(milliseconds: 500), // Default: 300ms
);
Storage Prefix #
Namespace your keys to avoid collisions:
class OnboardingProvider with StateSaver {
@override
String get storagePrefix => 'onboarding';
// Keys will be: 'onboarding_step', 'onboarding_user_data', etc.
}
Best Practices #
- Dispose properly - Always call
disposeSaveables()orsaveable.dispose()when done - Use prefixes - Namespace keys to avoid collisions between features
- Persist before exit - Call
persistAllNow()inAppLifecycleState.pausedfor critical data - Keep values serializable - Ensure your objects can be converted to/from JSON
License #
MIT License - see LICENSE for details.