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/home.dart | 6 +++++ lib/jotai.dart | 70 +++++++++++++++++++++++++++++++++++++------------------ lib/settings.dart | 8 +++++++ 3 files changed, 61 insertions(+), 23 deletions(-) (limited to 'lib') diff --git a/lib/home.dart b/lib/home.dart index f141d0b..fd2e278 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -30,6 +30,8 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { StreamSubscription? _positionStream; + // I was not able to get StreamBuilder or FutureBuilder to work correctly, + // so these are stored manually. bool _usesMetric = true; double _speed = 0.0; double _speedAccuracy = 0.0; @@ -72,6 +74,7 @@ class _HomePageState extends State { _initPositionStream(locationAccuracy); }); + // It takes time to load this data in, but I've only noticed it in debug mode RegionSettings.getUsesMetricSystem().then( (usesMetricSystem) => setState(() => _usesMetric = usesMetricSystem), ); @@ -88,6 +91,8 @@ class _HomePageState extends State { return Scaffold( appBar: AppBar( actions: [ + // This button is big enough and descriptive enough to be easily + // discoverable, but small enough to not get in the way. TextButton.icon( icon: Icon(Icons.settings), label: Text('Settings'), @@ -106,6 +111,7 @@ class _HomePageState extends State { (_usesMetric ? SpeedUnit.kilometersPerHour : SpeedUnit.milesPerHour); + return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, 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); } } diff --git a/lib/settings.dart b/lib/settings.dart index baaf0f4..4c60026 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -5,6 +5,9 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'main.dart'; +// Unfortunately, these need to be updated manually when a change is made to +// the version number or license. Storing these in code prevents the need for +// any loading. const applicationName = "Simple Speedometer"; const applicationVersion = "1.0.0"; const applicationLegalese = r""" @@ -22,6 +25,9 @@ class Option { const Option({required this.text, required this.value}); } +// I had to do some of the design work here myself, and this seems pretty close +// to what other Material apps do on Android. I tried the settings_ui package, +// but it doesn't respect the color scheme that I am using. class SectionHeader extends StatelessWidget { final String title; @@ -36,6 +42,8 @@ class SectionHeader extends StatelessWidget { } } +// Despite being part of the Material spec, the material package in Flutter +// does not have an alert widget to emulate this behavior. class SelectAlert extends StatefulWidget { final String title; final T initialValue; -- cgit v1.2.3