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);
}
}
SharedPreferences will lazy load the instance when it is needed but you can also initialize before and pass it in the constructor.
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.