// When I started this project, I knew I would eventually need either a library
// similar to streaming_shared_preferences, or a global state management system
// to cache the shared preferences. But I could not get that library to work,
// and I don't like any of the state management solutions that exist for
// Flutter.
//
// What I really wanted was something like jotai, and eventually I decided to
// just write my own. I'm very happy with how simple it is. It's just an
// Observable class, and a widget that listens for changes to the observable.
// It was very easy to extend it to save the preferences, and it's blazingly
// fast.
//
// There is a library called fl_observable that does essentially this but
// better. I didn't find it until after I finished the settings page, and it
// hasn't been updated for over a year. It also has very few users.
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:speedometer/main.dart';
String _defaultToString(dynamic value) => value.toString();
void Function(T) storageObserver<T>(
String key, [
String Function(T) toString = _defaultToString,
]) =>
// Loading the shared preferences for asll of these might not be
// particularly efficient, but it seems fine on my Moto G Stylus.
(T value) => SharedPreferencesAsync().setString(key, toString(value));
Future<T> fromString<T>(
String key,
List<T> options, [
String Function(T) toString = _defaultToString,
]) {
return SharedPreferencesAsync()
// This takes time to load in, but I've never noticed the delay
.getString(key)
.then(
// Going through all of these sounds like it's slower than necessary,
// but it's very simple, and most of the options lists are very small.
(value) => options.firstWhere((option) => value == toString(option)),
);
}
Observable<T> observablePreference<T>(
String key,
T defaultValue,
List<T> options, [
String Function(T) toString = _defaultToString,
]) => Observable.fromFuture(
initialValue: defaultValue,
future: fromString(key, options, toString),
observers: [storageObserver(key, toString)],
);
final speedUnitsObservable = observablePreference('speedUnits', null, [
...SpeedUnit.values,
null,
]);
final themeModeObservable = observablePreference(
'themeMode',
ThemeMode.system,
ThemeMode.values,
);
final primaryColorObservable = observablePreference(
"primaryColor",
Colors.red,
[...Colors.primaries, Colors.grey],
(color) => color.toARGB32().toString(),
);
final showMarginOfErrorObservable = observablePreference(
"showMarginOfError",
true,
[true, false],
);
final locationAccuracyObservable = observablePreference(
"locationAccuracy",
LocationAccuracy.best,
LocationAccuracy.values,
);
class Observable<T> {
T _value;
int _nextId = 0;
// A map is used instead of a list for fast deletions and to avoid memory
// leaks. Storing IDs in a list would make deletion slow, and setting entries
// in the list to null would cause memory leaks.
final Map<int, void Function(T)> _observers = {};
Observable(this._value, {List<void Function(T)>? observers}) {
if (observers != null) {
_observers.addAll(observers.asMap());
}
}
factory Observable.fromFuture({
required T initialValue,
required Future<T> future,
List<void Function(T)>? observers,
}) {
final self = Observable(initialValue, observers: observers);
future.then((value) => self.value = value);
return self;
}
T get value => _value;
set value(T value) {
if (value == _value) {
return;
}
_value = value;
for (var observer in _observers.values) {
observer(_value);
}
}
int subscribe(void Function(T) onChange) {
final id = _nextId++;
_observers[id] = onChange;
return id;
}
void unsubscribe(int subscriberId) {
_observers.remove(subscriberId);
}
}
class ObserverBuilder<T> extends StatefulWidget {
final Observable<T> observable;
final Widget Function(BuildContext, T, void Function(T)) builder;
const ObserverBuilder({
required this.observable,
required this.builder,
super.key,
});
@override
State<ObserverBuilder<T>> createState() => _ObserverState<T>();
}
class _ObserverState<T> extends State<ObserverBuilder<T>> {
late T _value;
late int _subscriberId;
@override
void initState() {
super.initState();
_subscriberId = widget.observable.subscribe(
(value) => setState(() => _value = value),
);
_value = widget.observable.value;
}
@override
void dispose() {
super.dispose();
widget.observable.unsubscribe(_subscriberId);
}
@override
Widget build(BuildContext context) {
return widget.builder(
context,
_value,
(value) => widget.observable.value = value,
);
}
}
|