From f5833f7f2fec30de0a58a6e7d989dd16c7787b74 Mon Sep 17 00:00:00 2001 From: Gregory Conrad Date: Tue, 25 Jul 2023 22:00:25 -0700 Subject: [PATCH] feat!: add Capsule.map and remove ListenerHandle --- packages/flutter_rearch/example/lib/main.dart | 27 ++++++----- packages/rearch/lib/rearch.dart | 45 ++++++++++--------- packages/rearch/test/basic_test.dart | 8 ++-- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/packages/flutter_rearch/example/lib/main.dart b/packages/flutter_rearch/example/lib/main.dart index deef85b..1f6c98f 100644 --- a/packages/flutter_rearch/example/lib/main.dart +++ b/packages/flutter_rearch/example/lib/main.dart @@ -8,7 +8,7 @@ import 'package:rearch/rearch.dart'; void main() => runApp(const TodoApp()); -/// Represents the [MimirIndex] that contains the movie dataset. +/// Represents the [MimirIndex] that contains the todos. Future indexAsyncCapsule(CapsuleHandle use) async { final instance = await Mimir.defaultInstance; return instance.openIndex('todos', primaryKey: 'timestamp'); @@ -23,9 +23,9 @@ AsyncValue indexWarmUpCapsule(CapsuleHandle use) { /// Acts as a proxy to the warmed-up [indexAsyncCapsule]. MimirIndex indexCapsule(CapsuleHandle use) { - return use(indexWarmUpCapsule).data.unwrapOrElse( - () => throw StateError('indexAsyncCapsule was not warmed up!'), - ); + return use(indexWarmUpCapsule).dataOrElse( + () => throw StateError('indexAsyncCapsule was not warmed up!'), + ); } /// Represents an item in the todos list. @@ -454,16 +454,15 @@ class TodoItem extends RearchConsumer { // The following uses a more advanced technique in rearch: inline capsules. // This is similar to `select` in other state management frameworks, but // inline capsules are much more powerful because they are full capsules. - // Please read the documentation to learn more about inline capsules - // *before using them*, because: - // > with great power comes great responsibility - // (you can accidentally cause leaks if you're not careful) + // Please read the documentation for more. final (:title, :description, :timestamp, :completed) = use( - (CapsuleReader use) => use(todoListCapsule).dataOrElse( - () => throw StateError( - 'In order to display a TodoItem, the todo list must have data!', - ), - )[index], + todoListCapsule.map( + (asyncList) => asyncList.dataOrElse( + () => throw StateError( + 'In order to display a TodoItem, the todo list must have data!', + ), + )[index], + ), ); final (:updateTodo, :deleteTodo) = use(todoListManagerCapsule); @@ -520,7 +519,7 @@ class DynamicBackground extends RearchConsumer { // We need to use this more advanced side effect since we need to be able // to grab the most up-to-date copy of the circles when we check to see // if we actually need to add a new circle (if we used the regular state - // effect, the closure would capture an outdated copy). + // effect, the closure would capture an outdated copy of the state). final (getCircles, setCircles) = use.stateGetterSetter({}); diff --git a/packages/rearch/lib/rearch.dart b/packages/rearch/lib/rearch.dart index 285ca42..47f2cfa 100644 --- a/packages/rearch/lib/rearch.dart +++ b/packages/rearch/lib/rearch.dart @@ -75,16 +75,21 @@ class CapsuleContainer implements Disposable { ) as _CapsuleManager; } - /// Reads the current data of the supplied [Capsule]. + /// Reads the current data of the supplied [Capsule], + /// initializing it if needed. T read(Capsule capsule) => _managerOf(capsule).data; /// *Temporarily* listens to changes in a given set of [Capsule]s. - /// If you want to listen to capsule(s) *not temporarily*, - /// instead just make an impure capsule and [read] it once to initialize it. - /// `listen` calls the supplied listener immediately, + /// + /// Calls the supplied [listener] immediately, /// and then after any capsules its listening to change. - @UseResult('ListenerHandle will leak its listener if it is not disposed') - ListenerHandle listen(CapsuleListener listener) { + /// You *must* call the returned dispose [Function] + /// when you no longer need the listener in order to prevent leaks. + /// + /// If you want to listen to capsule(s) *not temporarily*, instead just make + /// a non-idempotent capsule and [read] it once to initialize it. + @UseResult('Listener will leak in the container if it is not disposed') + void Function() listen(CapsuleListener listener) { // Create a temporary *non-idempotent* capsule so that it doesn't get // idempotent garbage collected void capsule(CapsuleHandle use) { @@ -93,9 +98,9 @@ class CapsuleContainer implements Disposable { } // Put the temporary capsule into the container so it gets data updates - read(capsule); + final manager = _managerOf(capsule); - return ListenerHandle._(this, capsule); + return manager.dispose; } /// Runs the supplied [callback] the next time [capsule] is @@ -113,9 +118,8 @@ class CapsuleContainer implements Disposable { // automatically disposed whenever the supplied capsule is updated/disposed // via the idempotent gc. void tempCapsule(CapsuleHandle use) => use(capsule); - final manager = _CapsuleManager(this, tempCapsule); + final manager = _managerOf(tempCapsule); manager.toDispose.add(callback); - _capsules[tempCapsule] = manager; return () { manager.toDispose.remove(callback); @@ -132,15 +136,14 @@ class CapsuleContainer implements Disposable { } } -/// A handle onto the lifecycle of a listener from [CapsuleContainer.listen]. -/// You *must* [dispose] the [ListenerHandle] -/// when you no longer need the listener in order to prevent leaks. -class ListenerHandle implements Disposable { - ListenerHandle._(this._container, this._capsule); - - final CapsuleContainer _container; - final _UntypedCapsule _capsule; - - @override - void dispose() => _container._capsules[_capsule]?.dispose(); +/// Provides the [map] convenience method on [Capsule]s. +extension CapsuleMapper on Capsule { + /// Maps this [Capsule] (of type [T]) into + /// a new idempotent [Capsule] (of type [R]) + /// by applying the given [mapper]. + /// + /// This is similar to `.select()` in some other libraries. + Capsule map(R Function(T) mapper) { + return (CapsuleReader use) => mapper(use(this)); + } } diff --git a/packages/rearch/test/basic_test.dart b/packages/rearch/test/basic_test.dart index be88756..93ebc09 100644 --- a/packages/rearch/test/basic_test.dart +++ b/packages/rearch/test/basic_test.dart @@ -97,19 +97,19 @@ void main() { void listener(CapsuleReader use) => states.add(use(stateful).$1); setState(1); - final handle1 = container.listen(listener); + final dispose1 = container.listen(listener); setState(2); setState(3); - handle1.dispose(); + dispose1(); setState(4); setState(5); - final handle2 = container.listen(listener); + final dispose2 = container.listen(listener); setState(6); setState(7); - handle2.dispose(); + dispose2(); setState(8); expect(states, equals([1, 2, 3, 5, 6, 7]));