diff options
Diffstat (limited to 'lib/jotai.dart')
| -rw-r--r-- | lib/jotai.dart | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/lib/jotai.dart b/lib/jotai.dart new file mode 100644 index 0000000..8e97fe8 --- /dev/null +++ b/lib/jotai.dart @@ -0,0 +1,190 @@ +// 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) { + try { + return options.firstWhere((option) => value == toString(option)); + } catch (error, stack) { + logObservable.value = logObservable.value + ..add('While loading $key:\n$error\n$stack'); + rethrow; + } + }, + ); +} + +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, +); + +final logObservable = Observable(<String>[]); +void log(String message) { + logObservable.value = logObservable.value..add(message); +} + +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) { + for (var observer in observers) { + subscribe(observer); + } + } + } + + 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, + ); + } +} |
