saveable 0.1.0 copy "saveable: ^0.1.0" to clipboard
saveable: ^0.1.0 copied to clipboard

Automatic variable-level state persistence.

Saveable #

pub package License: MIT

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 ValueListenable for easy widget binding
  • Storage agnostic - Bring your own storage backend (HydratedBloc, Hive, SharedPreferences, etc.)
  • Lifecycle management - Built-in disposal and cleanup via StateSaver mixin

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 #

  1. Dispose properly - Always call disposeSaveables() or saveable.dispose() when done
  2. Use prefixes - Namespace keys to avoid collisions between features
  3. Persist before exit - Call persistAllNow() in AppLifecycleState.paused for critical data
  4. Keep values serializable - Ensure your objects can be converted to/from JSON

License #

MIT License - see LICENSE for details.

1
likes
150
points
104
downloads

Publisher

verified publisherspiercer.tech

Weekly Downloads

Automatic variable-level state persistence.

Repository (GitHub)
View/report issues

Topics

#state-management #persistence #storage #hydration

Documentation

API reference

Funding

Consider supporting this project:

github.com

License

MIT (license)

Dependencies

flutter

More

Packages that depend on saveable