From ac5dcc8859aebfd2f36d4b6ac58f1bd2f82f4666 Mon Sep 17 00:00:00 2001 From: Mica White Date: Wed, 14 Jan 2026 20:36:39 -0500 Subject: Code cleanup --- lib/jotai.dart | 70 +++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 23 deletions(-) (limited to 'lib/jotai.dart') diff --git a/lib/jotai.dart b/lib/jotai.dart index a415e13..16de4e7 100644 --- a/lib/jotai.dart +++ b/lib/jotai.dart @@ -1,3 +1,19 @@ +// 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'; @@ -9,9 +25,10 @@ String _defaultToString(dynamic value) => value.toString(); void Function(T) storageObserver( String key, [ String Function(T) toString = _defaultToString, -]) { - return (T value) => SharedPreferencesAsync().setString(key, toString(value)); -} +]) => + // 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 fromString( String key, @@ -19,8 +36,11 @@ Future fromString( 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)), ); } @@ -30,9 +50,9 @@ Observable observablePreference( T defaultValue, List options, [ String Function(T) toString = _defaultToString, -]) => Observable( - defaultValue, - loadValue: fromString(key, options, toString), +]) => Observable.fromFuture( + initialValue: defaultValue, + future: fromString(key, options, toString), observers: [storageObserver(key, toString)], ); @@ -64,20 +84,26 @@ final locationAccuracyObservable = observablePreference( class Observable { T _value; - final List _observers = []; + 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 _observers = {}; - Observable( - this._value, { - Future? loadValue, - List? observers, - }) { + Observable(this._value, {List? observers}) { if (observers != null) { - _observers.addAll(observers); + _observers.addAll(observers.asMap()); } + } - if (loadValue != null) { - loadValue.then((value) => this.value = value); - } + factory Observable.fromFuture({ + required T initialValue, + required Future future, + List? observers, + }) { + final self = Observable(initialValue, observers: observers); + future.then((value) => self.value = value); + return self; } T get value => _value; @@ -88,21 +114,19 @@ class Observable { _value = value; - for (var observer in _observers) { - if (observer != null) { - observer(_value); - } + for (var observer in _observers.values) { + observer(_value); } } int subscribe(void Function(T) onChange) { - final id = _observers.length; - _observers.add(onChange); + final id = _nextId++; + _observers[id] = onChange; return id; } void unsubscribe(int subscriberId) { - _observers[subscriberId] = null; + _observers.remove(subscriberId); } } -- cgit v1.2.3