summaryrefslogtreecommitdiff
path: root/lib/jotai.dart
blob: 16de4e774afb15493c7763cc517d226da1eeb159 (plain)
// 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) => options.firstWhere((option) => value == toString(option)),
      );
}

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,
);

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) {
      _observers.addAll(observers.asMap());
    }
  }

  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,
    );
  }
}