summaryrefslogtreecommitdiff
path: root/lib/jotai.dart
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-09-28 19:23:35 -0400
committerMica White <botahamec@outlook.com>2026-09-28 19:23:35 -0400
commit1c8fee757e6125ff510f07106fb6304e89b2fad5 (patch)
tree7b1d4b885ded011de77fc77389d86de0ffaf7a05 /lib/jotai.dart
Condense all commits into one commit
At some point, I think I committed the Flutter binary. It's too deep to fix it now. At this point, I think my only solution is to delete my .git directory and re-initialize it.
Diffstat (limited to 'lib/jotai.dart')
-rw-r--r--lib/jotai.dart190
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,
+ );
+ }
+}