// 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( 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 fromString( String key, List 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 observablePreference( String key, T defaultValue, List 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 _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 _observers = {}; Observable(this._value, {List? observers}) { if (observers != null) { for (var observer in observers) { subscribe(observer); } } } 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; 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 extends StatefulWidget { final Observable observable; final Widget Function(BuildContext, T, void Function(T)) builder; const ObserverBuilder({ required this.observable, required this.builder, super.key, }); @override State> createState() => _ObserverState(); } class _ObserverState extends State> { 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, ); } }