Persisted Signals
When you need to store the state of your signals between app launches you can create a PersistedSignal
from this example code.
You need to have a store that can be SharedPreferences, SQLite, in memory, or any other storage solution. The store just needs to be able to save and restore the data.
abstract class KeyValueStore { Future<void> setItem(String key, String value); Future<String?> getItem(String key); Future<void> removeItem(String key);}
You can create an in-memory store for testing:
class InMemoryStore implements KeyValueStore { final Map<String, String> _store = {};
@override Future<String?> getItem(String key) async { return _store[key]; }
@override Future<void> removeItem(String key) async { _store.remove(key); }
@override Future<void> setItem(String key, String value) async { _store[key] = value; }}
For this example we are going to be using SharedPreferences:
class SharedPreferencesStore implements KeyValueStore { SharedPreferencesStore();
SharedPreferences? prefs;
Future<SharedPreferences> init() async { prefs ??= await SharedPreferences.getInstance(); return prefs!; }
@override Future<String?> getItem(String key) async { final prefs = await init(); return prefs.getString(key); }
@override Future<void> removeItem(String key) async { final prefs = await init(); prefs.remove(key); }
@override Future<void> setItem(String key, String value) async { final prefs = await init(); prefs.setString(key, value); }}
By default we can encode and decode the value to and from JSON:
abstract class PersistedSignal<T> extends FlutterSignal<T> with PersistedSignalMixin<T> { PersistedSignal( super.internalValue, { super.autoDispose, super.debugLabel, required this.key, required this.store, });
@override final String key;
@override final KeyValueStore store;}
mixin PersistedSignalMixin<T> on Signal<T> { String get key; KeyValueStore get store;
bool loaded = false;
Future<void> init() async { try { final val = await load(); super.value = val; } catch (e) { debugPrint('Error loading persisted signal: $e'); } finally { loaded = true; } }
@override T get value { if (!loaded) init().ignore(); return super.value; }
@override set value(T value) { super.value = value; save(value).ignore(); }
Future<T> load() async { final val = await store.getItem(key); if (val == null) return value; return decode(val); }
Future<void> save(T value) async { final str = encode(value); await store.setItem(key, str); }
T decode(String value) => jsonDecode(value);
String encode(T value) => jsonEncode(value);}
This can work in a lot of cases, but we might want to handle specific cases like enums:
class EnumSignal<T extends Enum> extends PersistedSignal<T> { EnumSignal(super.val, String key, this.values) : super( key: key, store: SharedPreferencesStore(), );
final List<T> values;
@override T decode(String value) => values.firstWhere((e) => e.name == value);
@override String encode(T value) => value.name;}
Or if you are in Flutter we can persist color values:
class ColorSignal extends PersistedSignal<Color> { ColorSignal(super.val, String key) : super( key: key, store: SharedPreferencesStore(), );
@override String encode(Color value) => value.value.toString();
@override Color decode(String value) => Color(int.parse(value));}
Example
class AppTheme { final sourceColor = ColorSignal( Colors.blue, 'sourceColor', ); final themeMode = EnumSignal( ThemeMode.system, 'themeMode', ThemeMode.values, );
static AppTheme instance = AppTheme();
Future<void> init() async { await Future.wait([ sourceColor.init(), themeMode.init(), ]); }}
void main() async{ final theme = AppTheme.instance; // We need to init before running the app to prevent the theme from flickering await theme.init(); runApp(MyApp());}
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final theme = AppTheme.instance; return MaterialApp( theme: ThemeData.light().copyWith( colorScheme: ColorScheme.fromSeed( seedColor: theme.sourceColor.watch(context), brightness: Brightness.light, ), ), darkTheme: ThemeData.dark().copyWith( colorScheme: ColorScheme.fromSeed( seedColor: theme.sourceColor.watch(context), brightness: Brightness.dark, ), ), themeMode: theme.themeMode.watch(context), home: Scaffold( appBar: AppBar( title: Text('Persisted Signals'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ ElevatedButton( onPressed: () { theme.sourceColor.value = Colors.red; }, child: Text('Change Color'), ), ElevatedButton( onPressed: () { theme.themeMode.value = ThemeMode.dark; }, child: Text('Change Theme'), ), ], ), ), ), ); }}
Now when we run the app and make changes, if we close the app and reopen it, the changes will persist offline.