Skip to content

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.