From 5e195a73c85ad53d896b4198a975fa56f7539e68 Mon Sep 17 00:00:00 2001 From: jinyus Date: Thu, 30 May 2024 08:17:11 -0500 Subject: [PATCH 1/4] add state_beacon_flutter for use without lite_tef --- packages/state_beacon_flutter/.gitignore | 10 + packages/state_beacon_flutter/CHANGELOG.md | 910 ++++++++++++++++++ packages/state_beacon_flutter/LICENSE | 21 + packages/state_beacon_flutter/README.md | 19 + .../analysis_options.yaml | 19 + .../state_beacon_flutter/example/README.md | 71 ++ .../lib/src/controller/controller.dart | 27 + .../lib/src/extensions/extensions.dart | 46 + .../lib/src/extensions/readable.dart | 9 + .../lib/src/extensions/value_notifier.dart | 36 + .../lib/src/extensions/watch_observe.dart | 132 +++ .../lib/src/extensions/writable.dart | 13 + .../src/notifier/text_editing_controller.dart | 79 ++ .../lib/src/scheduler.dart | 81 ++ .../lib/src/value_notifier_beacon.dart | 26 + .../lib/state_beacon_flutter.dart | 8 + packages/state_beacon_flutter/pubspec.yaml | 34 + .../state_beacon_flutter/test/common.dart | 2 + .../test/src/controller/controller_test.dart | 36 + .../test/src/edge_case_test.dart | 173 ++++ .../test/src/extensions/readable_test.dart | 77 ++ .../src/extensions/value_notifier_test.dart | 27 + .../test/src/extensions/writable_test.dart | 111 +++ .../test/src/flutter_test.dart | 298 ++++++ .../text_editing_controller_test.dart | 66 ++ .../test/src/value_notifier_beacon_test.dart | 28 + run.sh | 17 + 27 files changed, 2376 insertions(+) create mode 100644 packages/state_beacon_flutter/.gitignore create mode 100644 packages/state_beacon_flutter/CHANGELOG.md create mode 100644 packages/state_beacon_flutter/LICENSE create mode 100644 packages/state_beacon_flutter/README.md create mode 100644 packages/state_beacon_flutter/analysis_options.yaml create mode 100644 packages/state_beacon_flutter/example/README.md create mode 100644 packages/state_beacon_flutter/lib/src/controller/controller.dart create mode 100644 packages/state_beacon_flutter/lib/src/extensions/extensions.dart create mode 100644 packages/state_beacon_flutter/lib/src/extensions/readable.dart create mode 100644 packages/state_beacon_flutter/lib/src/extensions/value_notifier.dart create mode 100644 packages/state_beacon_flutter/lib/src/extensions/watch_observe.dart create mode 100644 packages/state_beacon_flutter/lib/src/extensions/writable.dart create mode 100644 packages/state_beacon_flutter/lib/src/notifier/text_editing_controller.dart create mode 100644 packages/state_beacon_flutter/lib/src/scheduler.dart create mode 100644 packages/state_beacon_flutter/lib/src/value_notifier_beacon.dart create mode 100644 packages/state_beacon_flutter/lib/state_beacon_flutter.dart create mode 100644 packages/state_beacon_flutter/pubspec.yaml create mode 100644 packages/state_beacon_flutter/test/common.dart create mode 100644 packages/state_beacon_flutter/test/src/controller/controller_test.dart create mode 100644 packages/state_beacon_flutter/test/src/edge_case_test.dart create mode 100644 packages/state_beacon_flutter/test/src/extensions/readable_test.dart create mode 100644 packages/state_beacon_flutter/test/src/extensions/value_notifier_test.dart create mode 100644 packages/state_beacon_flutter/test/src/extensions/writable_test.dart create mode 100644 packages/state_beacon_flutter/test/src/flutter_test.dart create mode 100644 packages/state_beacon_flutter/test/src/notifier/text_editing_controller_test.dart create mode 100644 packages/state_beacon_flutter/test/src/value_notifier_beacon_test.dart diff --git a/packages/state_beacon_flutter/.gitignore b/packages/state_beacon_flutter/.gitignore new file mode 100644 index 00000000..4996d54b --- /dev/null +++ b/packages/state_beacon_flutter/.gitignore @@ -0,0 +1,10 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock + +build/ +coverage/ diff --git a/packages/state_beacon_flutter/CHANGELOG.md b/packages/state_beacon_flutter/CHANGELOG.md new file mode 100644 index 00000000..9124023c --- /dev/null +++ b/packages/state_beacon_flutter/CHANGELOG.md @@ -0,0 +1,910 @@ +# 1.0.0 + +- [Breaking] Extracted the flutter specific code into a separate package `state_beacon_flutter` + +# 0.45.2 + +- [Fix] Edge case for Subscriptions + +# 0.45.1 + +- [Dependency] Updated `lite_ref` to `0.8.1` + +# 0.45.0 + +- [Feat] Add `ValueNotifier.toBeacon()` which converts a `ValueNotifier` to a `WritableBeacon`. All changes to the notifier are reflected in the beacon and vice versa. +- [Feat] Add `TextEditingBeacon` which is a beacon that wraps a `TextEditingController`. All changes to the controller are reflected in the beacon and vice versa. + + ## lite_ref (0.7.0): + + - [Breaking] The `overrides` property of `LiteRefScope` is now a `Set` instead of a `List`. + +# 0.44.5 + +- [Fix] Fix bug with `FutureBeacon`s not autosleeping +- [Feat] Expose list of beacons created in a BeaconGroup wuth `BeaconGroup.beacons` +- [Feat] Add `BeaconGroup.onCreate` to allow adding a callback to be run when a beacon is created + +# 0.44.4 + +- [Fix] Rare bug in FutureBeacon when start is called multiple times synchronously. + +# 0.44.3 + +- [Feat] Add `FutureBeacon.idle()` to set a beacon to the `AsyncIdle` state. + +# 0.44.2 + +- [Feat] Add the ability for widgets to observe beacons synchronously. When `synchronous` is true, `autobatching` will be disabled and all updates will be emitted immediately. + ```dart + final beacon = Beacon.writable(10); + beacon.observe(context, (prev, next) {}, synchronous: true); + ``` +- [Dependency] Updated `lite_ref` to `0.6.3` + +# 0.44.1 + +- [Refactor] Internal refactor +- [Dependency] Updated `lite_ref` to `0.6.2` + +# 0.44.0 + +- [Breaking] The `beacons` getter for `Beacon.family` has been replaced with `entries`. This is a breaking change because it returns a `MapEntry` instead of a `Beacon`. + +- [Dependency] Updated `lite_ref` to `0.6.1` + +# 0.43.0 + +- [Fix] Update lite_ref dependency and add flutter dependency constraint + +# 0.42.1 + +- [Fix] Export PeriodicBeacon and BeaconFamily classes + +# 0.42.0 + +- [Breaking] `resetIfError` option for `toFuture()` is now `true` by default. This was done because there's rarely a case where you'd want it to throw instantly. If you want to keep the previous value, set `resetIfError` to `false`. + +# 0.41.3 + +- [Docs] Update README + +# 0.41.2 + +- [Feat] Export `lite_ref` as the recommended dependency injection mechanism for state_beacon. Added convenience methods to `ScopedRef` for `BeaconController`s and `Beacon`s + + ```dart + class CountController extends BeaconController { + late final count = B.writable(0); + late final doubledCount = B.derived(() => count.value * 2); + } + + final countControllerRef = Ref.scoped((ctx) => CountController()); + + class CounterText extends StatelessWidget { + const CounterText({super.key}); + + @override + Widget build(BuildContext context) { + // watch the count beacon and return its value + final count = countControllerRef.select(context, (c) => c.count); + return Text('$count'); + } + } + ``` + +# 0.41.1 + +- [Refactor] Move `BeaconController` to `state_beacon_core` package + +# 0.41.0 + +- [Feat] Add `Beacon.periodic` that emits values periodically. + + ```dart + final myBeacon = Beacon.periodic(Duration(seconds: 1), (i) => i + 1); + + final nextFive = await myBeacon.buffer(5).next(); + + expect(nextFive, [1, 2, 3, 4, 5]); + ``` + +- [Breaking] `FutureBeacon.toFuture()` now returns immediately when it's not in the loading state. This is breaking because in previous versions, it would wait for the next update before returning the value. This was a bug! To get the next state you can use `.next()`. +- [Feat] `FutureBeacon.toFuture()` now has a `resetIfError` option that will reset the beacon if the current state is `AsyncError`. + +# 0.40.0 + +- [Feat] Add `BeaconController` for use in Flutter. see [docs](https://github.com/jinyus/dart_beacon/blob/main/packages/state_beacon/README.md#beaconcontroller) +- [Feat] Implement `Disposable` from [basic_interfaces](https://pub.dev/packages/basic_interfaces) package which makes it autodispsable when used with the [lite_ref](https://pub.dev/packages/lite_ref) package. +- [Docs] Add section on `testing` to the README. +- [Feat] Add `synchronous` option to wrap and chaining methods. This defaults to `true` which means that wrapper beacons will get all updates. +- [Breaking] Duration is now a positional argument for chaining methods `yourBeacon.debounce()`, `yourBeacon.throttle()`, `yourBeacon.bufferTime()`. This was done to make the code more concise. + + - Old: + + ```dart + final myBeacon = Beacon.writable(0); + myBeacon.debounce(duration: k10ms); + myBeacon.throttle(duration: k10ms); + myBeacon.bufferTime(duration: k10ms); + ``` + + - New: + + ```dart + final myBeacon = Beacon.writable(0); + myBeacon.debounce(k10ms); + myBeacon.throttle(k10ms); + myBeacon.bufferTime(k10ms); + ``` + +- [Deprecation] `yourBeacon.stream` is now `yourBeacon.toStream()`. This was done to allow auto-batching configuration. By default, auto-batching is enabled. You can disable it by setting `synchronous` to `true`. + + - Old: + + ```dart + final myBeacon = Beacon.writable(0); + myBeacon.stream; + ``` + + - New: + + ```dart + final myBeacon = Beacon.writable(0); + myBeacon.toStream(); + ``` + +# 0.39.1 + +- Minor refactor to improve performance. +- Add `BeaconObserver.useLogging()` as an alias to `BeaconObserver.instance = LoggingObserver()`. +- Reduce sdk constraint to ^3.0.0 from ^3.1.5 + +# 0.39.0 + +- [Breaking] Beacons will no longer be reset when disposed. It will keep its current value. +- [Breaking] Writing to a disposed beacon will throw an error. Reading will print a warning to the console in debug mode. A beacon should only be disposed if you have no more use for it. If you want to reuse a beacon, use the `reset` method instead. + + ```dart + final a = Beacon.writable(10); + a.dispose(); + a.value = 20; // throws an error + print(a.value); // prints 10 + ``` + +- [Breaking] When a beacon is disposed, all downstream derived beacons and effects will be disposed as well. + + ```dart + final a = Beacon.writable(10); + final b = Beacon.writable(10); + final c = Beacon.derived(() => a.value * b.value); + + a.subscribe((_) {}); + + Beacon.effect( + () { + c.value; + }, + name: 'effect', + ); + + //...// + + a.dispose(); + + // "c" is watching "a" so it is disposed + // the effect is watching "c" so it is disposed + // + // a b + // | / + // | / + // c + // | + // effect + + expect(a.isDisposed, true); + expect(c.isDisposed, true); + // effect is also disposed + ``` + +# 0.38.0 + +- [Feat] Add methods to `Beacon.streamRaw` that operates on the internal stream: `unsubscribe` `pause` and `resume`. +- [Feat] `onDispose` now returns a function that can be used to unregister the dispose listener. +- [Breaking] `anybeacon.next()` no longer takes a timeout parameter. It will also throw an error if called on a lazy beacon and the beacon is disposed before emitting a value; unless a [fallback] value is provided. + +Removed Deprecated methods: +`anybeacon.toStream()` is now removed. Use `anybeacon.stream` instead. + +# 0.37.0 + +- [Breaking] Remove `unsubscribe` method from `Beacon.streamRaw` +- [Fix] Bug when using Flutter scheduler where effects were not running before `runApp` was called. +- [Refactor] Internal refactor + +# 0.36.0 + +- [Breaking] `Beacon.stream` and `Beacon.streamRaw` will now autosleep when they have no more listeners. This is a breaking change because it changes the default behavior. If you want to keep the old behavior, set `shouldSleep` to false. + +They will unsubscribe from the stream when sleeping and resubscribe when awoken. For `Beacon.stream`, it will enter the loading state when awoken. It is recommended to use the default when using services like Firebase to prevent cost overruns. + +# 0.35.0 + +- [Breaking] The filter function is now required when chaining the filtered beacon. + +### old: + +```dart +final count = Beacon.writable(10); +final filtered = count.filter(filter: (prev, next) => next.isEven); +``` + +### new: + +```dart +final count = Beacon.writable(10); +final filtered = count.filter((prev, next) => next.isEven); +``` + +# 0.34.4 + +- Internal refactor + +# 0.34.3 + +- [Feat] Add `map` to chaining methods + +```dart +final count = Beacon.writable(10); +final mapped = count.map((value) => value * 2); + +expect(mapped.value, 20); + +count.value = 20; + +expect(count.value, 20); +expect(mapped.value, 40); +``` + +```dart +final stream = Stream.periodic(k1ms, (i) => i).take(5); +final beacon = stream + .toRawBeacon(isLazy: true) + .filter((_, n) => n.isEven) + .map((v) => v + 1) + .throttle(duration: k1ms); + +await expectLater(beacon.stream, emitsInOrder([1, 3, 5])); +``` + +See [docs](https://github.com/jinyus/dart_beacon?tab=readme-ov-file#mybeaconmap) for more information. + +# 0.34.2 + +- [Feat] Expose the list of beacons as a `Readable>` in the family beacon's cache. + + ```dart + final myFamily = Beacon.family((int id) => Beacon.writable(0)); + final beacons1 = family(1); + + Beacon.effect((){ + print('cache updated: ${myFamily.beacons.value}'); + }); + + final beacons2 = family(2); + // prints: cache updated: [beacons1, beacons2] + ``` + +# 0.34.1 + +- [Refactor] Internal refactor and minor improvement in performance + +# 0.34.0 + +- [Breaking] `Beacon.stream` and `Beacon.streamRaw` now takes a function that returns a stream instead of a stream directly. The upside of this change is that they are now derived beacons. All beacons accessed in the function will be tracked as dependencies. This means that if one of their dependencies changes, it will unsubscribe from the old stream and subscribe to the new one. This is a breaking change because it changes the signature of the method. + +- [Feat] Use can now manually start a stream beacon. It will start in the idle state when `manualStart` is true. + +- [Deprecated] `Beacon.derivedStream` is now deprecated. Use `Beacon.streaRaw` instead. +- [Deprecated] `Beacon.derivedFuture` is now deprecated. Use `Beacon.future` instead. +- [Deprecated] `Beacon.batch` is now deprecated. Batching is automatic with the [new core](#new-core). +- [Breaking] `cancelRunning` is now removed from `FutureBeacon`. It is now the default behavior. + +## New Core: + +This is a major update with many breaking changes. The core of state_beacon was rewritten from scratch to be more efficient and to support more use-cases. + +Pros: + +- Automatic batching +- Asynchronous by default +- Better performance for deep dependency trees/circular dependencies +- Scheduler customization + A scheduler is just a function that decides when to run all queued effects(flushing). By default, flushing is done with a microtask from DARTVM. This can be customized depending on your use case. For example, the flutter package ships with a scheduler that uses flutter's SchedulerBinding to handle flushing; as well as a 60fps scheduler that limits flushing to 60 times per second. Here is how you'd use them + +```dart +BeaconScheduler.useFlutterScheduler(); +BeaconScheduler.use60fpsScheduler(); +``` + +For flutter apps, it's recommended to use the flutter scheduler. The method must be called in the main function of your app. + +```dart +void main() { + BeaconScheduler.useFlutterScheduler(); + + runApp(const MyApp()); +} +``` + +Cons: + +- Default asynchrony introduces an inconvenience with testing. Effects are queued and the scheduler decides when to flush the queue. This is ideal for apps but makes testing harder because you have to manually flush the effect queue after updating a beacon to run all effects that depends on it. This can be done by calling `BeaconScheduler.flush()` after updating the beacon. + +> [!NOTE] +> This only applies to pure dart tests. In widgets tests, calling `tester.pumpAndSettle()` will flush the queue. + +```dart +final a = Beacon.writable(10); +var called = 0; + +// effect is queued for execution. The scheduler decides when to run the effect +Beacon.effect(() { + print("current value: ${a.value}"); + called++; +}); + +// manually flush the queue to run the all effect immediately +BeaconScheduler.flush(); + +expect(called, 1); + +a.value = 20; // effect will be queued again. + +BeaconScheduler.flush(); + +expect(called, 2); +``` + +# 0.33.6 + +- [Fix] Add bug fix from 0.34.0 to flutter package + +# 0.33.5 + +- [Fix] Bug with auto sleeping derivedFuture beacons + +# 0.33.4 + +- [Fix] Concurrent modification error when notifying listeners. + +# 0.33.3 + +- Allow a duration to be null in `ThrottledBeacon` and `DebouncedBeacon` to disable the throttle/debouncing. This makes them easier to test. + +# 0.33.2 + +- [Feat] Add `Beacon.derivedStream` + +Specialized `DerivedBeacon` that subscribes to the stream returned from its callback and updates its value based on the emitted values. +When a dependency changes, the beacon will unsubscribe from the old stream and subscribe to the new one. + +Example: + +```dart +final userID = Beacon.writable(18235); +final profileBeacon = Beacon.derivedStream(() { + return getProfileStreamFromUID(userID.value); +}); +``` + +# 0.33.1 + +- [Fix] All delegated writes will be forced to account for the fact that rollback isn't possible. + +# 0.33.0 + +- [Feat] Chaining beacons is now supported. When the beacon returned from a chain is mutated, the mutation is re-routed to the first beacon in the chain. + +```dart +final query = Beacon.writable(''); + +const k500ms = Duration(milliseconds: 500); + +final debouncedQuery = query + .filter((prev, next) => next.length > 2) + .debounce(duration: k500ms); +``` + +When `debouncedQuery` is mutated, the mutation is re-routed to `query`, then `filter` and finally `debounce`. + +NB: Buffered beacons cannot be mid-chain. If they are used, they must be the last beacon in the chain. + +```dart +// GOOD +someBeacon.filter().buffer(10); + +// BAD +someBeacon.buffer(10).filter(); +``` + +# 0.32.2 + +- Allow `initialValue` to be passed to `ingest` method + +# 0.32.1 + +- [Feat] Any writable can now wrap a stream with the new .ingest() method. + + ```dart + final myBeacon = Beacon.writable(0); + myBeacon.ingest(anyStream); + ``` + +- [Feat] `RawStreamBeacon`s can now be initialized lazily by setting the `isLazy` option to true. + +# 0.32.0 + +- [Feat] Add `.stream` getter for all beacons +- [Deprecation] `toStream()` is now deprecated. Use `.stream` instead. + + ```dart + // before + final myBeacon = Beacon.writable(0); + final myStream = myBeacon.toStream(); + + // after + final myBeacon = Beacon.writable(0); + final myStream = myBeacon.stream; + ``` + +# 0.31.2 + +- [Perf] This is an internal change. Only create 1 `StreamController` per beacon. +- [Deprecation] The `broadcast` option for `toStream()` is now deprecated as it's now redundant. + + ```dart + // before + final myBeacon = Beacon.writable(0); + final myStream = myBeacon.toStream(broadcast: true); + + // after + final myBeacon = Beacon.writable(0); + final myStream = myBeacon.toStream(); + ``` + +# 0.31.1 + +- [Fix] mirror force option of wrapped beacons. + +# 0.31.0 + +- [Breaking] Derived and DerivedFuture beacons will now enter a sleep state when a widget/effect watching it is unmounted/disposed. + + Currently derived & derivedFuture beacons always execute even if it has no listeners. + It will now enter a sleep state when nothing is watching it. + + Pro: No unneeded computation so it saves battery life. + Con: It will not have the latest state when a widget starts watching it again so it will be in the loading state when woken up. + + It is configurable with a `shouldSleep` option which defaults to true. + + NB: It still start eagerly, the above only kicks in when listeners decrease from 1 to 0. If you want a lazy start, just declare it as a late variable. + + ```dart + // with late keyword, someFuture won't run until stats is used. + late final stats = Beacon.derivedFuture(() async => someFuture()); + ``` + +# 0.30.0 + +- [Feat] Add BeaconGroup that allows you to group beacons together and dispose/reset them all at once. + + ```dart + final myGroup = BeaconGroup(); + + final name = myGroup.writable('Bob'); + final age = myGroup.writable(20); + + age.value = 21; + name.value = 'Alice'; + + myGroup.resetAll(); + + print(name.value); // Bob + print(age.value); // 20 + + myGroup.disposeAll(); + + print(name.isDisposed); // true + print(age.isDisposed); // true + ``` + +- [Fix] resetting an uninitialized lazybeacon no longer throws an error. + +# 0.29.3 + +- [Feat] Add ability to do optimistic update when using AsyncValue.tryCatch() + +# 0.29.2 + +- Internal refactor that enables deeply nested untracked withing nested effects. This is just covers a rare edge case and should not affect any existing code. + +# 0.29.1 + +- [Refactor] Internal refactor to improve performance + +# 0.29.0 + +- [Breaking] `toStream()` now has a broadcast option that defaults to false. This is a breaking change because it changes the default behavior of `toStream()`. If you want to keep the old behavior, set `broadcast` to true. + +# 0.28.0 + +- Internal refactor + +# 0.27.0 + +- [Breaking] rename `debugLabel` to `name` + +# 0.26.0 + +- [Breaking] beacons **NO** longer implements ValueListenable. Use `mybeacon.toListenable()` as a replacement. This was done because implementing ValueListenable necessitated importing the flutter package which isn't usable in pure dart projects. + +## Everything below was written before the state_beacon -> state_beacon_core migration + +The core of state_beacon was extracted into a separate package to make it usuable in pure dart projects. + +# 0.25.0 + +- [Deprecation] `Beacon.createEffect`. Use `Beacon.effect` instead. +- [Deprecation] `Beacon.doBatchUpdate`. Use `Beacon.batch` instead. +- Add `reset` for list,set and map beacons. +- add `isEmpty` getter that returns true if a lazy beacon is uninitialized. +- Internal refactor + +# 0.24.0 + +- [Breaking] Convenience wrapping methods: `.buffer()`, `.bufferTime()`, `.throttle()` and `.filter()` etc no longer needs an initial value. It fetches its initial value from the target beacon if `startNow` is true (which is the default). + +# 0.23.0 + +- [Breaking] beacon.wrap() no longer returns the wrapper instance. This is redundant as it retuned the same instance that the method was called on. Chanining can be achieved by using `beacon..wrap()..wrap()` + +# 0.22.1 + +- Add `disposeTogether` option for beacon wrapping. This will dispose all wrapped beacons when the wrapping beacon is disposed and vice versa. It's set the `false` for manual wrapping and `true` when using extension methods like `mybeacon.buffer(10)` + +# 0.22.0 + +- [Breaking] initialValue is now a named argument for all beacon _classes_. This doesn't affect any existing public api provided by `Beacon.writable()` etc. This only affects you if you are using the `Beacon` class directly. + +# 0.21.0 + +- [Breaking] Separate `idle` and `loading` states for the `isLoading` getter in `AsyncValue`. Use `isIdleOrLoading` for the old behavior. +- [Fix] BufferedBeacon.reset() no longer unsubscribes from the wrapped beacon. + +# 0.20.1 + +- Stacktrace is now optional in AsyncError contructor. StackTrace.current is used if it is not provided. + +# 0.20.0 + +- [Feat] Add effect actions to Observer and debuglabel for effects. +- [Feat] A function can be returned from effect closures that will be called when the effect is disposed. This can be used to clean up sub effects. +- [Fix] Effects no longer do additional passes to discover new beacons when supportCOnditional is false; +- [Breaking] `isNullable` is no longer exposed +- [Breaking] remove `reset` from stream and readable beacons. +- [Breaking] remove `start` from derived beacon. Use late initialization instead. + +# 0.19.2 + +- [Chore] Update documentation + +# 0.19.1 + +- [Feat] Add `.next()` to all beacons that exposes the next value as a future +- [Feat] Add `.buffer()` that returns a [BufferedCountBeacon] that wraps this Beacon. +- [Feat] Add `.bufferTime()` that returns a [BufferedTimeBeacon] that wraps this Beacon. +- [Feat] Add `.throttle()` that returns a [ThrottledBeacon] that wraps this Beacon. +- [Feat] Add `.filter()` that returns a [FilteredBeacon] that wraps this Beacon. + +# 0.19.0 + +- [Breaking] FamilyBeacon are cached by default + +# 0.18.4 + +- [Fix] Cached family beacons are removed from cache when they are disposed + +# 0.18.3 + +- StreamBeacon.reset() now sets loading state and resubscribes to the stream + +# 0.18.2 + +- [Fix] batch opperations that threw errors would leave the beacon in an inconsistent state. +- [Breaking] start requests to derived beacons that were already started will now be ignored instead of throwing an error + +# 0.18.1 + +- [Feature] Make beacons callable(`beacon()`) as an alternative to `beacon.value` + +# 0.18.0 + +- [Breaking] remove forceSetValue from DerivedBeacon public api + +# 0.17.1 + +- Make conditional listening configurable for `Beacon.createEffect` ,`Beacon.derived` and `Beacon.derivedFuture` + +# 0.17.0 + +- Mdd debugLabel to beacons +- Add BeaconObserver and LoggingObserver classes +- Add FutureBeacon.overrideWith() to replace the internal callback +- Add `AsyncValue.isData` and `AsyncValue.isError` getters +- Add shortcuts: `FutureBeacon.isData` and `FutureBeacon.isError` and `FutureBeacon.unwrapValue()` +- [Breaking] `AsyncValue.unwrapValue()` is now `AsyncValue.unwrap()` +- [Breaking] Make initialValue named argument for lazy beacons +- [Breaking] Make `filter` a named argument for FilteredBeacon +- [Breaking] Make `initialValue` required for ListBeacon +- [Breaking] `FutureBeacon.previousValue` is no longer customized to return the previous AsyncData, use `FutureBeacon.lastData` instead + +# 0.16.0 + +- beacon.toStream() now returns a broadcast stream +- Add lastData. isLoading and valueOrNull getters to AsyncValue +- Add optional beacon parameter to tryCatch +- Add WritableBeacon.tryCatch extension for handling asynchronous values + +# 0.15.0 + +- Beacon.untracked() now only hide the update/access from encompassing effects. + +# 0.14.6 + +- Add `tryCatch` method to AsyncValue class that executes a future and returns an AsyncData or AsyncError + +# 0.14.5 + +- Internal improvements + +# 0.14.4 + +- Add `WriteableBeacon.freeze()` that converts it to a `ReadableBeacon` + +# 0.14.3 + +- Internal refactor + +# 0.14.2 + +- Add Beacon.family +- Add myBeacon.onDispose to listen to when a beacon is disposed +- Add `onCancel` to `toSteam()` extension method + +# 0.14.1 + +- internal improvements + +# 0.14.0 + +- undeRedo: expose canUndo,canRedo and history +- Add isDisposed property to all beacons +- Add ThrottleBeacon.setDuration to change the throttle duration + +### Breaking Changes + +- Beacon.list no longer implements List. It only has mutating methods + +# 0.13.9 + +- add `WritableBeacon.clearWrapped()`` method to dispose all currently wrapped beacons + +# 0.13.8 + +- Revert change in 0.13.7 + +# 0.13.7 + +- Expose the internal completer on FutureBeacon. ie: `myFutureBeacon.completer` + +# 0.13.6 + +- Minor internal refactor + +# 0.13.5 + +- Minor internal improvements + +# 0.13.4 + +- Minor internal refactor + +# 0.13.3 + +- Internal improvements + +# 0.13.2 + +- Internal improvements + +# 0.13.1 + +- Internal improvements + +# 0.13.0 + +- `watch` and `observe` are now methods on Beacon instead of extensions + +# 0.12.11 + +- Add FilteredBeacon.hasFilter getter +- Fix previous Value in filteredBeacon filter callback + +# 0.12.10 + +- Add Beacon.untracked + +# 0.12.9 + +- Internal improvements + +# 0.12.8 + +- Fix null assertion bug in `Beacon.observe` + +# 0.12.7 + +- Add beacon.observe(context, callback) for performing side effects in a widget + +# 0.12.6 + +- TimestampBeacon now extend ReadableBeacon + +# 0.12.5 + +- Internal code refactor + +# 0.12.4 + +- Internal improvements + +# 0.12.3 + +- Internal fixes + +# 0.12.2 + +- Add myStreamBeacon.toFuture() that exposes a StreamBeacon as a Future +- Add Beacon.streamRaw that emits unwrapped values + +# 0.12.1 + +- Internal fixes + +# 0.12.0 + +- Beacon.asFuture is now FutureBeacon.toFuture() + +# 0.11.2 + +- Add Beacon.asFuture that exposes a FutureBeacon as a Future + +# 0.11.1 + +- Mark internal methods as @protected + +# 0.11.0 + +- FutureBeacon is now a base class for DefaultFutureBeacon and DerivedFutureBeacon +- Expose DerivedFutureBeacon as a FutureBeacon + +# 0.10.2 + +- Add unwrapValue() method to AsyncValue class +- Keep track of the last AsyncData so it can be used in loading and error states + +# 0.10.1 + +- Expose listenersCount +- Internal improvements + +# 0.10.0 + +- FilteredBeacon : Make filter function nullable which allows changing/setting it after initialization + +# 0.9.2 + +- Fix: refreshing logic for DerivedFutureBeacon +- Allow customization of how the old results of a future are handled in when it has be retriggered +- Add increment and decrement methods to Writable + +# 0.9.1 + +- Add initialValue getter +- Customize previousValue getter for DerivedFutureBeacon to ignore loading/error states +- Fix memory leak in BufferedBeacons + +# 0.9.0 + +- Roll flutter_state_beacon package into state_beacon package +- Add `watch` extension for use in flutter widgets +- Beacons now implement ValueListenable +- Add `toValueNotifier()` and `toStream()` extension methods + +# 0.8.0 + +- Avoid throwing errors when start is called on a beacon that is already started + +# 0.7.0 + +- Changed `startNow` to `manualStart` for future and derived beacons to avoid ambiguity + +# 0.6.1 + +- Expose `cancelOnError` option for StreamBeaon + +# 0.6.0 + +- Give all writable beacons a lazy variant +- Expose option to manually trigger futureBeacon execution +- Add option to manually trigger and reset derivedFutureBeacon +- Add option to manually trigger derivedBeacon +- Refactor Writable.wrap and remove redundant methods: wrapThen and wrapTransform +- Add BufferedBeacon.wrap + +# 0.5.0 + +- Add AsyncIdle State and ability to manually trigger futureBeacon execution +- Add ability to do lazy starts for wrap methods + +# 0.4.1 + +- Internal refactor + +# 0.4.0 + +- Add WritableBeacon.set that can force update listeners + +# 0.3.3 + +- Expose all Beacons + +# 0.3.2 + +- Expose ReadableBeacon and WritableBeacon + +# 0.3.1 + +- ThrottledBeacon: add method to change duration +- Add Writable.wrapThen +- Return dispose function for all `wrap` method + +# 0.3.0 + +- Expose currentBuffer for BufferedCountBeacon and BufferedTimeBeacon +- Fix bug in BufferedTimeBeacon.reset() +- Add UndoRedoBeacon + +# 0.2.1 + +- Add BufferedCountBeacon and BufferedTimeBeacon + +# 0.2.0 + +- Fix bug with DerivedBeacons unregistering +- Notify listeners when LazyBeacon is initialized +- Add `mapInPlace` for ListBeacon + +# 0.1.2 + +- Add `Beacon.scopedWritable`. + +# 0.1.1 + +- Update pubspec.yaml. + +# 0.1.0 + +- Initial version. diff --git a/packages/state_beacon_flutter/LICENSE b/packages/state_beacon_flutter/LICENSE new file mode 100644 index 00000000..62492abf --- /dev/null +++ b/packages/state_beacon_flutter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Remi Rousselet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/state_beacon_flutter/README.md b/packages/state_beacon_flutter/README.md new file mode 100644 index 00000000..12b527b6 --- /dev/null +++ b/packages/state_beacon_flutter/README.md @@ -0,0 +1,19 @@ +

+ +

+ +

+ + + + stars +

+ +> [!NOTE] +> This package is intended for those who want to use [state_beacon](https://pub.dev/packages/state_beacon) without its sister package, [lite_ref](https://pub.dev/packages/lite_ref), for dependency injection. + +## Overview + +A Beacon is a reactive primitive(`signal`) and simple state management solution for Dart and Flutter; `state_beacon` leverages the [node coloring technique](https://dev.to/modderme123/super-charging-fine-grained-reactive-performance-47ph) created by [Milo Mighdoll](https://twitter.com/modderme123) and used in the latest versions of [SolidJS](https://www.youtube.com/watch?v=jHDzGYHY2ew&t=5291s) and [reactively](https://github.com/modderme123/reactively). + +### See full documentation [here](https://pub.dev/packages/state_beacon). diff --git a/packages/state_beacon_flutter/analysis_options.yaml b/packages/state_beacon_flutter/analysis_options.yaml new file mode 100644 index 00000000..f092ea48 --- /dev/null +++ b/packages/state_beacon_flutter/analysis_options.yaml @@ -0,0 +1,19 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +# include: package:lints/recommended.yaml +include: package:very_good_analysis/analysis_options.yaml + +linter: + rules: + avoid_positional_boolean_parameters: false diff --git a/packages/state_beacon_flutter/example/README.md b/packages/state_beacon_flutter/example/README.md new file mode 100644 index 00000000..b80e4210 --- /dev/null +++ b/packages/state_beacon_flutter/example/README.md @@ -0,0 +1,71 @@ +source code with tests: [examples/counter/lib/main.dart](https://github.com/jinyus/dart_beacon/blob/main/examples/counter/lib/main.dart) + +```dart +class Controller extends BeaconController { + late final count = B.writable(0); + + void increment() => count.value++; + void decrement() => count.value--; +} + +final countController = Controller(); + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('State Beacon Counter without LiteRef'), + centerTitle: true, + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + ), + body: const Center(child: CounterText()), + floatingActionButton: const Buttons(), + ), + ); + } +} + +class CounterText extends StatelessWidget { + const CounterText({super.key}); + + @override + Widget build(BuildContext context) { + final count = countController.count; + final theme = Theme.of(context); + return Text('$count', style: theme.textTheme.displayLarge); + } +} + +class Buttons extends StatelessWidget { + const Buttons({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + IconButton.filled( + onPressed: countController.increment, + icon: const Icon(Icons.add), + iconSize: 32, + ), + const SizedBox(height: 8), + IconButton.filled( + onPressed: countController.decrement, + icon: const Icon(Icons.remove), + iconSize: 32, + ), + ], + ); + } +} +``` diff --git a/packages/state_beacon_flutter/lib/src/controller/controller.dart b/packages/state_beacon_flutter/lib/src/controller/controller.dart new file mode 100644 index 00000000..d235c0b2 --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/controller/controller.dart @@ -0,0 +1,27 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:flutter/widgets.dart'; +import 'package:state_beacon_core/state_beacon_core.dart'; + +/// A mixin that automatically disposes all beacons created by this Widget. +mixin BeaconControllerMixin on State { + /// Local BeaconCreator that automatically + /// disposes all beacons created within this State class. + /// + /// ### All beacons must be created with as a `late` variable. + /// ```dart + /// late final age = B.writable(50); + /// ^ + /// | + /// this is required + /// ``` + final B = BeaconGroup(); + + /// Disposes all beacons created by this controller. + @override + @mustCallSuper + void dispose() { + B.disposeAll(); + super.dispose(); + } +} diff --git a/packages/state_beacon_flutter/lib/src/extensions/extensions.dart b/packages/state_beacon_flutter/lib/src/extensions/extensions.dart new file mode 100644 index 00000000..d0c8135e --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/extensions/extensions.dart @@ -0,0 +1,46 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:state_beacon_core/state_beacon_core.dart'; +import 'package:state_beacon_flutter/src/value_notifier_beacon.dart'; + +part 'readable.dart'; +part 'watch_observe.dart'; +part 'writable.dart'; + +final Map> _vnCache = {}; + +@visibleForTesting + +/// The number of value notifiers currently in use +/// This is used for testing purposes only +bool hasNotifier(BaseBeacon beacon) { + return _vnCache.containsKey(beacon.hashCode); +} + +ValueNotifier _toValueNotifier(ReadableBeacon beacon) { + final key = beacon.hashCode; + + final existing = _vnCache[key]; + + if (existing != null) { + return existing as ValueNotifierBeacon; + } + + final notifier = ValueNotifierBeacon(beacon.peek()); + + _vnCache[key] = notifier; + + final unsub = beacon.subscribe(notifier.set, startNow: false); + + notifier.addDisposeCallback(() { + unsub(); + _vnCache.remove(key); + }); + + beacon.onDispose(() { + notifier.dispose(); + _vnCache.remove(key); + }); + + return notifier; +} diff --git a/packages/state_beacon_flutter/lib/src/extensions/readable.dart b/packages/state_beacon_flutter/lib/src/extensions/readable.dart new file mode 100644 index 00000000..56675153 --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/extensions/readable.dart @@ -0,0 +1,9 @@ +part of 'extensions.dart'; + +// ignore: public_member_api_docs +extension ReadableBeaconFlutterUtils on ReadableBeacon { + /// Converts this to a [ValueListenable] + ValueListenable toListenable() { + return _toValueNotifier(this); + } +} diff --git a/packages/state_beacon_flutter/lib/src/extensions/value_notifier.dart b/packages/state_beacon_flutter/lib/src/extensions/value_notifier.dart new file mode 100644 index 00000000..45cad389 --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/extensions/value_notifier.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; +import 'package:state_beacon_core/state_beacon_core.dart'; + +/// Extensions for [ValueNotifier]. +extension ValueNotifierUtils on ValueNotifier { + /// Converts this to a [WritableBeacon]. + WritableBeacon toBeacon({BeaconGroup? group, String? name}) { + final beaconCreator = group ?? Beacon; + + final beacon = beaconCreator.writable(value, name: name); + + var syncing = false; + void safeWrite(VoidCallback fn) { + if (syncing) return; + syncing = true; + try { + fn(); + } finally { + syncing = false; + } + } + + void update() => safeWrite(() => beacon.set(value)); + + addListener(update); + + beacon + ..subscribe( + (v) => safeWrite(() => value = v), + synchronous: true, + ) + ..onDispose(() => removeListener(update)); + + return beacon; + } +} diff --git a/packages/state_beacon_flutter/lib/src/extensions/watch_observe.dart b/packages/state_beacon_flutter/lib/src/extensions/watch_observe.dart new file mode 100644 index 00000000..4084c911 --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/extensions/watch_observe.dart @@ -0,0 +1,132 @@ +// ignore_for_file: invalid_use_of_protected_member, use_if_null_to_convert_nulls_to_bools, lines_longer_than_80_chars, deprecated_member_use + +part of 'extensions.dart'; + +// ignore: public_member_api_docs +typedef ObserverCallback = void Function(T prev, T next); + +// coverage:ignore-start +// requires a manual GC trigger to test +final Finalizer _finalizer = Finalizer((fn) => fn()); +// coverage:ignore-end + +/// @macro WidgetUtils +extension WidgetUtils on BaseBeacon { + /// Watches a beacon and triggers a widget + /// rebuild when its value changes. + /// + /// Note: must be called within a widget's build method. + /// + /// Usage: + /// ```dart + /// final counter = Beacon.writable(0); + /// + /// class Counter extends StatelessWidget { + /// const Counter({super.key}); + /// + /// @override + /// Widget build(BuildContext context) { + /// final count = counter.watch(context); + /// return Text(count.toString()); + /// } + ///} + /// ``` + T watch(BuildContext context) { + final key = context.hashCode; + + return _watchOrObserve( + key, + context, + ); + } + + /// Observes the state of a beacon and triggers a callback with the current state. + /// + /// The callback is provided with the current state of the beacon and a BuildContext. + /// This can be used to show snackbars or other side effects. + /// + /// Usage: + /// ```dart + /// final exampleBeacon = Beacon.writable("Initial State"); + /// + /// class ExampleWidget extends StatelessWidget { + /// @override + /// Widget build(BuildContext context) { + /// context.observe(exampleBeacon, (state, context) { + /// ScaffoldMessenger.of(context).showSnackBar( + /// SnackBar(content: Text(state)), + /// ); + /// }); + /// return Container(); + /// } + /// } + /// ``` + void observe( + BuildContext context, + ObserverCallback callback, { + bool synchronous = false, + }) { + final key = Object.hash( + context, + 'isObserving', // create 1 subscription for each widget + ); + + _watchOrObserve( + key, + context, + callback: () => callback(previousValue as T, peek()), + synchronous: synchronous, + ); + } + + T _watchOrObserve( + int key, + BuildContext context, { + VoidCallback? callback, + bool synchronous = false, + }) { + if ($$widgetSubscribers$$.contains(key)) { + return peek(); + } + + $$widgetSubscribers$$.add(key); + + final elementRef = WeakReference(context as Element); + late VoidCallback unsub; + + void rebuildWidget() { + elementRef.target!.markNeedsBuild(); + } + + final run = callback ?? rebuildWidget; + + void handleNewValue(T value) { + if (elementRef.target?.mounted == true) { + run(); + } else { + unsub(); + $$widgetSubscribers$$.remove(key); + } + } + + unsub = subscribe( + handleNewValue, + startNow: false, + synchronous: synchronous, + ); + + // coverage:ignore-start + // clean up if the widget is disposed + // and value is never modified again + _finalizer.attach( + context, + () { + unsub(); + $$widgetSubscribers$$.remove(key); + }, + ); + // coverage:ignore-end + + return peek(); + } +} diff --git a/packages/state_beacon_flutter/lib/src/extensions/writable.dart b/packages/state_beacon_flutter/lib/src/extensions/writable.dart new file mode 100644 index 00000000..56032b78 --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/extensions/writable.dart @@ -0,0 +1,13 @@ +part of 'extensions.dart'; + +/// @macro [WritableBeaconFlutterUtils] +extension WritableBeaconFlutterUtils on WritableBeacon { + /// Converts this to a [ValueNotifier] + ValueNotifier toValueNotifier() { + final notifier = _toValueNotifier(this); + + notifier.addListener(() => set(notifier.value)); + + return notifier; + } +} diff --git a/packages/state_beacon_flutter/lib/src/notifier/text_editing_controller.dart b/packages/state_beacon_flutter/lib/src/notifier/text_editing_controller.dart new file mode 100644 index 00000000..233acbc0 --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/notifier/text_editing_controller.dart @@ -0,0 +1,79 @@ +import 'package:flutter/widgets.dart'; +import 'package:state_beacon_core/state_beacon_core.dart'; + +/// This is a wrapper around a [TextEditingController] that +/// allows you to hook into the controller's lifecycle. +class _TextEditingController extends TextEditingController { + VoidCallback? disposeCallback; + bool _disposed = false; + + @override + void dispose() { + if (_disposed) return; + _disposed = true; + disposeCallback?.call(); + super.dispose(); + } +} + +/// A beacon that wraps a [TextEditingController]. +class TextEditingBeacon extends WritableBeacon { + /// @macro [TextEditingBeacon] + TextEditingBeacon({String? text, BeaconGroup? group, super.name}) + : super( + initialValue: text == null + ? TextEditingValue.empty + : TextEditingValue(text: text), + ) { + group?.add(this); + var syncing = false; + + void safeWrite(VoidCallback fn) { + if (syncing) return; + syncing = true; + try { + fn(); + } finally { + syncing = false; + } + } + + _controller.addListener(() { + safeWrite(() => set(_controller.value, force: true)); + }); + + subscribe( + (v) => safeWrite(() => _controller.value = v), + synchronous: true, + ); + + _controller.disposeCallback = dispose; + } + + late final _controller = _TextEditingController(); + + /// The current [TextEditingController]. + TextEditingController get controller => _controller; + + /// The current string the user is editing. + String get text => _controller.text; + + set text(String newText) { + _controller.text = newText; + } + + /// The currently selected [text]. + /// + /// If the selection is collapsed, then this property gives + /// the offset of the cursor within the text. + TextSelection get selection => _controller.selection; + + /// Alias for controller.clear() + void clear() => _controller.clear(); + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/packages/state_beacon_flutter/lib/src/scheduler.dart b/packages/state_beacon_flutter/lib/src/scheduler.dart new file mode 100644 index 00000000..68092cd2 --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/scheduler.dart @@ -0,0 +1,81 @@ +import 'package:flutter/scheduler.dart'; +import 'package:state_beacon_core/state_beacon_core.dart' as core; + +/// `Effects` are not synchronous, their execution is controlled by a scheduler. +/// When a dependency of an `effect` changes, it is added to a queue and +/// the scheduler decides when is the best time to flush the queue. +/// By default, the queue is flushed with a DARTVM microtask which runs +/// on the next loop; this can be changed by setting a custom scheduler. +/// Flutter comes with its own scheduler, so it is recommended to use +/// flutter's scheduler when using beacons in a flutter app. +/// This can be done by calling `BeaconScheduler.useFlutterScheduler();` +/// in the `main` function. +/// +/// ```dart +/// void main() { +/// BeaconScheduler.useFlutterScheduler(); +/// +/// runApp(const MyApp()); +/// } +/// ``` +abstract class BeaconScheduler { + /// Runs all queued effects/subscriptions + /// This is made available for testing and should not be used in production + static void flush() => core.BeaconScheduler.flush(); + + /// This scheduler uses the Flutter SchedulerBinding to + /// schedule updates to be processed after the current frame. + static void useFlutterScheduler() { + _flushing = false; + core.BeaconScheduler.setScheduler(_flutterScheduler); + } + + /// This scheduler limits the frequency that updates + /// are processed to 60 times per second. + static void use60fpsScheduler() { + core.BeaconScheduler.use60fpsScheduler(); + } + + /// This scheduler limits the frequency that updates + /// are processed to a custom fps. + static void useCustomFpsScheduler(int updatesPerSecond) { + core.BeaconScheduler.useCustomFpsScheduler(updatesPerSecond); + } + + /// Sets the scheduler to the provided function + // coverage:ignore-start + static void setCustomScheduler(void Function() scheduler) { + core.BeaconScheduler.setScheduler(scheduler); + } + // coverage:ignore-end + + /// This scheduler processes updates synchronously. This is not recommended + /// for production apps and only provided to make testing easier. + /// + /// With this scheduler, you aren't protected from stackoverflows when + /// an effect mutates a beacon that it depends on. This is a infinite loop + /// with the sync scheduler. + // static void useSyncScheduler() { + // core.BeaconScheduler.useSyncScheduler(); + // } +} + +var _flushing = false; + +void _flutterScheduler() { + if (_flushing) return; + _flushing = true; + if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { + Future.microtask(() { + core.BeaconScheduler.flush(); + _flushing = false; + }); + } else { + // coverage:ignore-start + SchedulerBinding.instance.addPostFrameCallback((_) { + core.BeaconScheduler.flush(); + _flushing = false; + }); + // coverage:ignore-end + } +} diff --git a/packages/state_beacon_flutter/lib/src/value_notifier_beacon.dart b/packages/state_beacon_flutter/lib/src/value_notifier_beacon.dart new file mode 100644 index 00000000..707ab26e --- /dev/null +++ b/packages/state_beacon_flutter/lib/src/value_notifier_beacon.dart @@ -0,0 +1,26 @@ +// ignore_for_file: public_member_api_docs, use_setters_to_change_properties + +import 'package:flutter/foundation.dart'; + +/// @macro [ValueNotifierBeacon] +class ValueNotifierBeacon extends ValueNotifier { + ValueNotifierBeacon(super.value); + + final disposeListeners = []; + + void addDisposeCallback(VoidCallback callback) { + disposeListeners.add(callback); + } + + void set(T newValue) { + value = newValue; + } + + @override + void dispose() { + for (final cb in disposeListeners) { + cb(); + } + super.dispose(); + } +} diff --git a/packages/state_beacon_flutter/lib/state_beacon_flutter.dart b/packages/state_beacon_flutter/lib/state_beacon_flutter.dart new file mode 100644 index 00000000..7163983e --- /dev/null +++ b/packages/state_beacon_flutter/lib/state_beacon_flutter.dart @@ -0,0 +1,8 @@ +/// reactive primitive and statemanagement for flutter +library; + +export 'package:state_beacon_core/state_beacon_core.dart' hide BeaconScheduler; +export 'src/controller/controller.dart'; +export 'src/extensions/extensions.dart' hide hasNotifier; +export 'src/notifier/text_editing_controller.dart'; +export 'src/scheduler.dart'; diff --git a/packages/state_beacon_flutter/pubspec.yaml b/packages/state_beacon_flutter/pubspec.yaml new file mode 100644 index 00000000..e346db95 --- /dev/null +++ b/packages/state_beacon_flutter/pubspec.yaml @@ -0,0 +1,34 @@ +name: state_beacon_flutter +description: A reactive primitive and simple state managerment solution for dart and flutter +version: 1.0.0 +repository: https://github.com/jinyus/dart_beacon + +environment: + sdk: ^3.0.0 + flutter: ">=3.17.0-1.0.pre.38" + +dependencies: + flutter: + sdk: flutter + state_beacon_core: ^1.0.0 + +dev_dependencies: + flutter_lints: ^3.0.0 + flutter_test: + sdk: flutter + lints: ^3.0.0 + very_good_analysis: ^5.1.0 + +platforms: + android: + ios: + linux: + macos: + web: + windows: + +topics: + - state-management + - signal + - reactive + - beacon diff --git a/packages/state_beacon_flutter/test/common.dart b/packages/state_beacon_flutter/test/common.dart new file mode 100644 index 00000000..0bcc9554 --- /dev/null +++ b/packages/state_beacon_flutter/test/common.dart @@ -0,0 +1,2 @@ +const k1ms = Duration(milliseconds: 1); +const k10ms = Duration(milliseconds: 10); diff --git a/packages/state_beacon_flutter/test/src/controller/controller_test.dart b/packages/state_beacon_flutter/test/src/controller/controller_test.dart new file mode 100644 index 00000000..042be96c --- /dev/null +++ b/packages/state_beacon_flutter/test/src/controller/controller_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/state_beacon_flutter.dart'; + +void main() { + testWidgets('should dispose all beacons in State class', (tester) async { + await tester.pumpWidget(const CoounterView()); + final state = tester.state<_CoounterViewState>(find.byType(CoounterView)); + + expect(state.count.isDisposed, false); + expect(state.doubledCount.isDisposed, false); + + await tester.pumpWidget(const SizedBox.shrink()); + + expect(state.count.isDisposed, true); + expect(state.doubledCount.isDisposed, true); + }); +} + +class CoounterView extends StatefulWidget { + const CoounterView({super.key}); + + @override + State createState() => _CoounterViewState(); +} + +class _CoounterViewState extends State + with BeaconControllerMixin { + late final count = B.writable(0); + late final doubledCount = B.derived(() => count.value * 2); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/packages/state_beacon_flutter/test/src/edge_case_test.dart b/packages/state_beacon_flutter/test/src/edge_case_test.dart new file mode 100644 index 00000000..17148570 --- /dev/null +++ b/packages/state_beacon_flutter/test/src/edge_case_test.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/state_beacon_flutter.dart'; + +typedef Themes = ({ThemeData lightTheme}); + +class Customization { + const Customization({ + this.themes = const {}, + }); + + final Map themes; +} + +class PrefController { + PrefController({ + required this.defaultThemeName, + required this.appCustomization, + }); + + final String? defaultThemeName; + final Customization appCustomization; + + late final selectedTheme = Beacon.writable( + defaultThemeName ?? 'Default', + name: 'selectedTheme', + ); + + late final lightTheme = Beacon.derived( + () { + final selectedThemeName = selectedTheme.value; + return appCustomization.themes[selectedThemeName]?.lightTheme; + }, + name: 'lightTheme', + ); +} + +const color1 = Color(0xFF000000); +const color2 = Color(0xFFFF0000); +const colorFallback = Color(0xFFFFFFFF); + +const myWidgetKey = Key('myWidgetKey'); + +void main() { + testWidgets('Should change theme with empty', (widgetTester) async { + // BeaconObserver.instance = LoggingObserver(); + final controller = PrefController( + defaultThemeName: null, + appCustomization: Customization( + themes: { + '1': (lightTheme: ThemeData.light().copyWith(primaryColor: color1)), + '2': (lightTheme: ThemeData.light().copyWith(primaryColor: color2)), + }, + ), + ); + + await widgetTester.pumpWidget(MyApp(controller: controller)); + expect(controller.selectedTheme.peek(), 'Default'); + var byKey = find.byKey(myWidgetKey); + expect(byKey.first, findsOneWidget); + expect(widgetTester.widget(byKey).color, colorFallback); + + controller.selectedTheme.value = '2'; + await widgetTester.pumpAndSettle(); + expect(controller.selectedTheme.peek(), '2'); + byKey = find.byKey(myWidgetKey); + expect(byKey, findsOneWidget); + expect(widgetTester.widget(byKey).color, color2); + + controller.selectedTheme.value = '1'; + await widgetTester.pumpAndSettle(); + expect(controller.selectedTheme.peek(), '1'); + byKey = find.byKey(myWidgetKey); + expect(byKey, findsOneWidget); + expect(widgetTester.widget(byKey).color, color1); + }); + + testWidgets('Should change theme with invalid predefined value', + (widgetTester) async { + // BeaconObserver.instance = LoggingObserver(); + final controller = PrefController( + defaultThemeName: 'value not exists', + appCustomization: Customization( + themes: { + '1': (lightTheme: ThemeData.light().copyWith(primaryColor: color1)), + '2': (lightTheme: ThemeData.light().copyWith(primaryColor: color2)), + }, + ), + ); + + await widgetTester.pumpWidget(MyApp(controller: controller)); + expect(controller.selectedTheme.peek(), 'value not exists'); + var byKey = find.byKey(myWidgetKey); + expect(byKey.first, findsOneWidget); + expect(widgetTester.widget(byKey).color, colorFallback); + + controller.selectedTheme.value = '2'; + await widgetTester.pumpAndSettle(); + expect(controller.selectedTheme.peek(), '2'); + byKey = find.byKey(myWidgetKey); + expect(byKey, findsOneWidget); + expect(widgetTester.widget(byKey).color, color2); + + controller.selectedTheme.value = '1'; + await widgetTester.pumpAndSettle(); + expect(controller.selectedTheme.peek(), '1'); + byKey = find.byKey(myWidgetKey); + expect(byKey, findsOneWidget); + expect(widgetTester.widget(byKey).color, color1); + }); + + testWidgets('Should change theme with predefined value', + (widgetTester) async { + final controller = PrefController( + defaultThemeName: '1', + appCustomization: Customization( + themes: { + '1': (lightTheme: ThemeData.light().copyWith(primaryColor: color1)), + '2': (lightTheme: ThemeData.light().copyWith(primaryColor: color2)), + }, + ), + ); + + // BeaconObserver.useLogging(); + + await widgetTester.pumpWidget(MyApp(controller: controller)); + expect(controller.selectedTheme.peek(), '1'); + var byKey = find.byKey(myWidgetKey); + expect(byKey.first, findsOneWidget); + expect(widgetTester.widget(byKey).color, color1); + + controller.selectedTheme.value = '2'; + await widgetTester.pumpAndSettle(); + expect(controller.selectedTheme.peek(), '2'); + byKey = find.byKey(myWidgetKey); + expect(byKey, findsOneWidget); + expect(widgetTester.widget(byKey).color, color2); + + controller.selectedTheme.value = '1'; + await widgetTester.pumpAndSettle(); + expect(controller.selectedTheme.peek(), '1'); + byKey = find.byKey(myWidgetKey); + expect(byKey, findsOneWidget); + expect(widgetTester.widget(byKey).color, color1); + }); +} + +class MyApp extends StatelessWidget { + const MyApp({ + required this.controller, + super.key, + }); + + final PrefController controller; + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: controller.lightTheme.watch(context) ?? + ThemeData.light().copyWith(primaryColor: colorFallback), + home: Builder( + builder: (ctx) { + return Scaffold( + body: ColoredBox( + key: myWidgetKey, + color: Theme.of(ctx).primaryColor, + ), + ); + }, + ), + ); + } +} diff --git a/packages/state_beacon_flutter/test/src/extensions/readable_test.dart b/packages/state_beacon_flutter/test/src/extensions/readable_test.dart new file mode 100644 index 00000000..9403fc8f --- /dev/null +++ b/packages/state_beacon_flutter/test/src/extensions/readable_test.dart @@ -0,0 +1,77 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/src/extensions/extensions.dart'; +import 'package:state_beacon_flutter/state_beacon_flutter.dart'; + +void main() { + test('should convert to a value listenable', () { + final beacon = Beacon.writable(0); + + final valueNotifier = beacon.toListenable(); + + expect(valueNotifier, isA>()); + + var called = 0; + + int fn() => called++; + valueNotifier.addListener(fn); + + beacon.value = 1; + + BeaconScheduler.flush(); + + expect(called, 1); + + beacon.value = 2; + + BeaconScheduler.flush(); + + expect(called, 2); + + valueNotifier.removeListener(fn); + + beacon.value = 3; + + BeaconScheduler.flush(); + + expect(called, 2); + }); + + test('should return the same listener instance', () { + final beacon = Beacon.writable(0); + + final valueNotifier = beacon.toListenable(); + final valueNotifier2 = beacon.toListenable(); + final valueNotifier3 = beacon.toListenable(); + + expect(valueNotifier, valueNotifier2); + expect(valueNotifier2, valueNotifier3); + + expect(hasNotifier(beacon), isTrue); + + beacon.dispose(); + + expect(hasNotifier(beacon), isFalse); + }); + + test('should remove notifier from cache when source is disposed.', () { + final age = Beacon.writable(50); + final name = Beacon.writable('bob'); + + age.toListenable(); + name.toListenable(); + + expect(hasNotifier(age), isTrue); + expect(hasNotifier(name), isTrue); + + age.dispose(); + + expect(hasNotifier(age), isFalse); + expect(hasNotifier(name), isTrue); + + name.dispose(); + + expect(hasNotifier(age), isFalse); + expect(hasNotifier(name), isFalse); + }); +} diff --git a/packages/state_beacon_flutter/test/src/extensions/value_notifier_test.dart b/packages/state_beacon_flutter/test/src/extensions/value_notifier_test.dart new file mode 100644 index 00000000..cdd602de --- /dev/null +++ b/packages/state_beacon_flutter/test/src/extensions/value_notifier_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/src/extensions/value_notifier.dart'; +import 'package:state_beacon_flutter/state_beacon_flutter.dart'; + +void main() { + test('should be synced with source value notifier', () async { + final notifier = ValueNotifier(0); + final beacon = notifier.toBeacon(); + + expect(beacon.peek(), 0); + + notifier.value = 1; + + expect(beacon.value, 1); + + notifier.value = 2; + + expect(beacon.value, 2); + + beacon.increment(); + + expect(beacon.value, 3); + + expect(notifier.value, 3); + }); +} diff --git a/packages/state_beacon_flutter/test/src/extensions/writable_test.dart b/packages/state_beacon_flutter/test/src/extensions/writable_test.dart new file mode 100644 index 00000000..666e1f47 --- /dev/null +++ b/packages/state_beacon_flutter/test/src/extensions/writable_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/src/extensions/extensions.dart'; +import 'package:state_beacon_flutter/state_beacon_flutter.dart'; + +void main() { + test('should convert to a value notifier', () { + final beacon = Beacon.writable(0); + + final valueNotifier = beacon.toValueNotifier(); + + expect(valueNotifier, isA>()); + + var called = 0; + + valueNotifier.addListener(() => called++); + + beacon.value = 1; + + BeaconScheduler.flush(); + + expect(called, 1); + + beacon.value = 2; + + BeaconScheduler.flush(); + + expect(called, 2); + + valueNotifier.dispose(); + + beacon.value = 3; + + BeaconScheduler.flush(); + + expect(called, 2); + }); + + test('should remove listeners source beacon is disposed', () { + BeaconScheduler.use60fpsScheduler(); // just to test it out for coverage + + final beacon = Beacon.writable(0); + + final valueNotifier = beacon.toValueNotifier(); + + expect(valueNotifier, isA>()); + + var called = 0; + + valueNotifier.addListener(() => called++); + + beacon.value = 1; + + BeaconScheduler.flush(); + + expect(called, 1); + + beacon.value = 2; + + BeaconScheduler.flush(); + + expect(called, 2); + + beacon.dispose(); + + // ignore: invalid_use_of_protected_member + expect(valueNotifier.hasListeners, false); + }); + + test('should return the same notifier instance', () { + final beacon = Beacon.writable(0); + + final valueNotifier = beacon.toValueNotifier(); + final valueNotifier2 = beacon.toValueNotifier(); + final valueNotifier3 = beacon.toValueNotifier(); + + expect(valueNotifier, valueNotifier2); + expect(valueNotifier2, valueNotifier3); + + expect(hasNotifier(beacon), isTrue); + + beacon.dispose(); + + expect(hasNotifier(beacon), isFalse); + }); + + test('should remove notifier from cache when notifier is disposed.', () { + final age = Beacon.writable(50); + final name = Beacon.writable('bob'); + + final aNotifier = age.toValueNotifier(); + final nNotifier = name.toValueNotifier(); + + expect(hasNotifier(age), isTrue); + expect(hasNotifier(name), isTrue); + expect(age.listenersCount, 1); + expect(name.listenersCount, 1); + + aNotifier.dispose(); + + expect(hasNotifier(age), isFalse); + expect(hasNotifier(name), isTrue); + expect(age.listenersCount, 0); + + nNotifier.dispose(); + + expect(hasNotifier(age), isFalse); + expect(hasNotifier(name), isFalse); + expect(name.listenersCount, 0); + }); +} diff --git a/packages/state_beacon_flutter/test/src/flutter_test.dart b/packages/state_beacon_flutter/test/src/flutter_test.dart new file mode 100644 index 00000000..20dcfbcb --- /dev/null +++ b/packages/state_beacon_flutter/test/src/flutter_test.dart @@ -0,0 +1,298 @@ +// ignore_for_file: cascade_invocations + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/state_beacon_flutter.dart'; + +import '../common.dart'; + +void main() { + BeaconScheduler.useFlutterScheduler(); + testWidgets('should rebuild Counter widget when count changes', + (WidgetTester tester) async { + final counter = Beacon.writable(0); + + final widget = Counter(counter: counter); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: widget, + ), + ), + ); + + expect(find.text('0'), findsOneWidget); + + counter.increment(); + + await tester.pumpAndSettle(); + + // Verify updated state + expect(find.text('1'), findsOneWidget); + expect(widget.builtCount, 2); + }); + + testWidgets('should rebuild FutureCounter on state changes', + (WidgetTester tester) async { + BeaconScheduler.useCustomFpsScheduler(60); + // BeaconObserver.instance = LoggingObserver(); + final counter = Beacon.writable(0, name: 'counter'); + + final derivedFutureCounter = Beacon.future( + () async { + final count = counter.value; + return counterFuture(count); + }, + name: 'derived', + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureCounter(derived: derivedFutureCounter), + ), + ), + k10ms, + ); + + await tester.pumpAndSettle(); + + expect(find.text('${counter.value} second has passed.'), findsOneWidget); + + counter.increment(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureCounter(derived: derivedFutureCounter), + ), + ), + k10ms * 2, + ); + + await tester.pumpAndSettle(); + + // Verify loading indicator + expect(find.text('${counter.value} second has passed.'), findsOneWidget); + + counter.value = 5; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FutureCounter(derived: derivedFutureCounter), + ), + ), + k10ms * 2, + ); + + await tester.pumpAndSettle(); + + expect( + find.text('Exception: Count(${counter.value}) too large'), + findsOneWidget, + ); + }); + + testWidgets('should show snackbar for exceeding 3', + (WidgetTester tester) async { + // Build the Counter widget + final counter = Beacon.writable(0); + final widget = Counter(counter: counter); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: widget, + ), + ), + ); + + // Increase counter beyond limit + counter.value = 4; + await tester.pumpAndSettle(); + + // Verify snackbar visibility + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('Count cannot be greater than 3'), findsOneWidget); + expect(widget.observedCount, 1); + }); + + testWidgets('should batch calls observe ', (WidgetTester tester) async { + final counter = Beacon.writable(0); + final widget = Counter(counter: counter); + await tester.pumpWidget(MaterialApp(home: Scaffold(body: widget))); + + counter.increment(); + counter.increment(); + counter.increment(); + + await tester.pumpAndSettle(); + + expect(widget.observedCount, 1); + }); + + testWidgets('should call observe synchronously', (WidgetTester tester) async { + final counter = Beacon.writable(0); + final widget = Counter(counter: counter, synchronous: true); + await tester.pumpWidget(MaterialApp(home: Scaffold(body: widget))); + + counter.increment(); + counter.increment(); + counter.increment(); + + await tester.pumpAndSettle(); + + expect(widget.observedCount, 3); + }); + + testWidgets('should show snackbar for going negative', + (WidgetTester tester) async { + // Build the Counter widget + final counter = Beacon.writable(0); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Counter(counter: counter), + ), + ), + ); + + // Decrease counter below limit + counter.value = -1; + await tester.pumpAndSettle(); + + // Verify snackbar visibility + expect(find.byType(SnackBar), findsOneWidget); + expect(find.text('Count cannot be negative'), findsOneWidget); + }); + + testWidgets('listenersCount decreases on widget dispose', + (WidgetTester tester) async { + // BeaconObserver.instance = LoggingObserver(); + final testCounter = Beacon.writable(0, name: 'testCounter'); + // BeaconScheduler.setScheduler(flutterScheduler); + + // Check initial listeners count + expect(testCounter.listenersCount, 0); + + // Build the 5 Counter widget, each with 2 listeners + // 1 for the text and 1 for the observer + await tester.pumpWidget( + MaterialApp( + home: Column( + children: [ + Counter(counter: testCounter), + Counter(counter: testCounter), + Counter(counter: testCounter), + Counter(counter: testCounter), + Counter(counter: testCounter), + ], + ), + ), + ); + + // Check listeners count after widget is built + expect(testCounter.listenersCount, 10); + + // Dispose the Counter widget by pumping a different widget + await tester.pumpWidget(const MaterialApp(home: SizedBox())); + + testCounter.value = 1; + + await tester.pumpAndSettle(); + + // Check listeners count after widget is disposed + expect(testCounter.listenersCount, 0); + + testCounter.dispose(); + }); +} + +Future counterFuture(int count) async { + if (count > 3) { + throw Exception('Count($count) too large'); + } + + await Future.delayed(Duration(milliseconds: count * 10)); + return '$count second has passed.'; +} + +class CounterColumn extends StatelessWidget { + const CounterColumn({required this.counter, required this.show, super.key}); + + final WritableBeacon counter; + final WritableBeacon show; + + @override + Widget build(BuildContext context) { + return Column( + children: show.watch(context) + ? [ + Counter(counter: counter), + Counter(counter: counter), + Counter(counter: counter), + Counter(counter: counter), + Counter(counter: counter), + ] + : [Container()], + ); + } +} + +// ignore: must_be_immutable +class Counter extends StatelessWidget { + Counter({required this.counter, super.key, this.synchronous = false}); + + final bool synchronous; + final WritableBeacon counter; + + int builtCount = 0; + int observedCount = 0; + + @override + Widget build(BuildContext context) { + builtCount++; + counter.observe( + context, + (prev, next) { + observedCount++; + final messenger = ScaffoldMessenger.of(context); + messenger.clearSnackBars(); + if (next > prev && next > 3) { + messenger.showSnackBar( + const SnackBar( + content: Text('Count cannot be greater than 3'), + ), + ); + } else if (next < prev && next < 0) { + messenger.showSnackBar( + const SnackBar( + content: Text('Count cannot be negative'), + ), + ); + } + }, + synchronous: synchronous, + ); + return Text( + counter.watch(context).toString(), + style: Theme.of(context).textTheme.headlineMedium, + ); + } +} + +class FutureCounter extends StatelessWidget { + const FutureCounter({required this.derived, super.key}); + + final FutureBeacon derived; + + @override + Widget build(BuildContext context) { + return switch (derived.watch(context)) { + AsyncData(value: final v) => Text(v), + AsyncError(error: final e) => Text('$e'), + _ => const CircularProgressIndicator(), + }; + } +} diff --git a/packages/state_beacon_flutter/test/src/notifier/text_editing_controller_test.dart b/packages/state_beacon_flutter/test/src/notifier/text_editing_controller_test.dart new file mode 100644 index 00000000..701e9043 --- /dev/null +++ b/packages/state_beacon_flutter/test/src/notifier/text_editing_controller_test.dart @@ -0,0 +1,66 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/state_beacon_flutter.dart'; + +void main() { + test('should be synced with internal TextEditingController', () async { + final beacon = TextEditingBeacon(); + final controller = beacon.controller; + + expect(beacon.peek(), TextEditingValue.empty); + expect(beacon.text, ''); + expect(controller.text, ''); + + beacon.text = '1'; + + expect(beacon.text, '1'); + expect(controller.text, '1'); + + beacon.text = '2'; + + expect(beacon.text, '2'); + expect(controller.text, '2'); + + beacon.clear(); + + expect(beacon.text, ''); + expect(controller.text, ''); + + controller.text = '3'; + + expect(beacon.text, '3'); + expect(controller.text, '3'); + + controller.clear(); + + expect(beacon.text, ''); + expect(controller.text, ''); + + controller.dispose(); + + expect(beacon.isDisposed, true); + }); + + test('should reflect changes to the controller', () async { + final beacon = TextEditingBeacon(text: '1'); + final controller = beacon.controller; + + expect(beacon.text, '1'); + expect(controller.text, '1'); + + expect(beacon.selection, controller.selection); + }); + + test('should add beacon to group provided', () { + final group = BeaconGroup(); + final beacon = TextEditingBeacon(text: '1', group: group); + + expect(group.beacons.length, 1); + expect(group.beacons.first, beacon); + + group.disposeAll(); + + expect(group.beacons.isEmpty, true); + + expect(beacon.isDisposed, true); + }); +} diff --git a/packages/state_beacon_flutter/test/src/value_notifier_beacon_test.dart b/packages/state_beacon_flutter/test/src/value_notifier_beacon_test.dart new file mode 100644 index 00000000..29f339ef --- /dev/null +++ b/packages/state_beacon_flutter/test/src/value_notifier_beacon_test.dart @@ -0,0 +1,28 @@ +// ignore_for_file: invalid_use_of_protected_member + +import 'package:flutter_test/flutter_test.dart'; +import 'package:state_beacon_flutter/src/value_notifier_beacon.dart'; + +void main() { + test('should notify listener and call dispose callbacks', () { + final beacon = ValueNotifierBeacon(0); + var called = 0; + + void disposeTest() => called++; + + beacon.addListener(() => called++); + + expect(beacon.value, 0); + + beacon.set(1); + + expect(beacon.value, 1); + expect(called, 1); + + beacon + ..addDisposeCallback(disposeTest) + ..dispose(); + + expect(called, 2); + }); +} diff --git a/run.sh b/run.sh index a0648f5b..0d26d5b8 100755 --- a/run.sh +++ b/run.sh @@ -15,10 +15,17 @@ test_target() { echo "testing core" cd packages/state_beacon_core && flutter test --coverage --timeout 5s + elif [ "$1" == "flutter" ]; then echo "testing flutter" + cd packages/state_beacon_flutter && + flutter test --coverage + + elif [ "$1" == "main" ]; then + echo "testing main" cd packages/state_beacon && flutter test --coverage + elif [ "$1" == "example" ]; then echo "testing flutter_main example" cd examples/flutter_main && @@ -57,8 +64,14 @@ publish_target() { elif [ "$1" == "flutter" ]; then echo "publishing flutter" + cd packages/state_beacon_flutter && + publish_and_update_pubignore + + elif [ "$1" == "main" ]; then + echo "publishing main" cd packages/state_beacon && publish_and_update_pubignore + elif [ "$1" == "lint" ]; then echo "publishing lint" cd packages/state_beacon_lints @@ -73,6 +86,8 @@ deps() { flutter pub get && cd $CURRENT_DIR/packages/state_beacon && flutter pub get && + cd $CURRENT_DIR/packages/state_beacon_flutter && + flutter pub get && cd $CURRENT_DIR/examples/flutter_main && flutter pub get && cd $CURRENT_DIR/examples/counter && @@ -84,6 +99,8 @@ deps() { cd $CURRENT_DIR/examples/auth_flow && flutter pub get && cd $CURRENT_DIR/examples/skeleton && + flutter pub get && + cd $CURRENT_DIR/examples/github_search && flutter pub get } From 8a3743c310756492943aaa8b111e0ad8fbeefa47 Mon Sep 17 00:00:00 2001 From: jinyus Date: Thu, 30 May 2024 08:19:42 -0500 Subject: [PATCH 2/4] remove duplicate files state_beacon --- .../lib/src/controller/controller.dart | 27 -- .../lib/src/extensions/extensions.dart | 48 --- .../lib/src/extensions/readable.dart | 9 - .../lib/src/extensions/scoped_ref.dart | 3 +- .../lib/src/extensions/value_notifier.dart | 36 --- .../lib/src/extensions/watch_observe.dart | 132 -------- .../lib/src/extensions/writable.dart | 13 - .../src/notifier/text_editing_controller.dart | 79 ----- packages/state_beacon/lib/src/scheduler.dart | 81 ----- .../lib/src/value_notifier_beacon.dart | 26 -- packages/state_beacon/lib/state_beacon.dart | 7 +- .../test/src/controller/controller_test.dart | 36 --- .../state_beacon/test/src/edge_case_test.dart | 173 ---------- .../test/src/extensions/readable_test.dart | 77 ----- .../src/extensions/value_notifier_test.dart | 27 -- .../test/src/extensions/writable_test.dart | 111 ------- .../state_beacon/test/src/flutter_test.dart | 298 ------------------ .../text_editing_controller_test.dart | 66 ---- .../test/src/value_notifier_beacon_test.dart | 28 -- 19 files changed, 4 insertions(+), 1273 deletions(-) delete mode 100644 packages/state_beacon/lib/src/controller/controller.dart delete mode 100644 packages/state_beacon/lib/src/extensions/extensions.dart delete mode 100644 packages/state_beacon/lib/src/extensions/readable.dart delete mode 100644 packages/state_beacon/lib/src/extensions/value_notifier.dart delete mode 100644 packages/state_beacon/lib/src/extensions/watch_observe.dart delete mode 100644 packages/state_beacon/lib/src/extensions/writable.dart delete mode 100644 packages/state_beacon/lib/src/notifier/text_editing_controller.dart delete mode 100644 packages/state_beacon/lib/src/scheduler.dart delete mode 100644 packages/state_beacon/lib/src/value_notifier_beacon.dart delete mode 100644 packages/state_beacon/test/src/controller/controller_test.dart delete mode 100644 packages/state_beacon/test/src/edge_case_test.dart delete mode 100644 packages/state_beacon/test/src/extensions/readable_test.dart delete mode 100644 packages/state_beacon/test/src/extensions/value_notifier_test.dart delete mode 100644 packages/state_beacon/test/src/extensions/writable_test.dart delete mode 100644 packages/state_beacon/test/src/flutter_test.dart delete mode 100644 packages/state_beacon/test/src/notifier/text_editing_controller_test.dart delete mode 100644 packages/state_beacon/test/src/value_notifier_beacon_test.dart diff --git a/packages/state_beacon/lib/src/controller/controller.dart b/packages/state_beacon/lib/src/controller/controller.dart deleted file mode 100644 index 6b8c4505..00000000 --- a/packages/state_beacon/lib/src/controller/controller.dart +++ /dev/null @@ -1,27 +0,0 @@ -// ignore_for_file: non_constant_identifier_names - -import 'package:flutter/widgets.dart'; -import 'package:state_beacon/state_beacon.dart'; - -/// A mixin that automatically disposes all beacons created by this Widget. -mixin BeaconControllerMixin on State { - /// Local BeaconCreator that automatically - /// disposes all beacons created within this State class. - /// - /// ### All beacons must be created with as a `late` variable. - /// ```dart - /// late final age = B.writable(50); - /// ^ - /// | - /// this is required - /// ``` - final B = BeaconGroup(); - - /// Disposes all beacons created by this controller. - @override - @mustCallSuper - void dispose() { - B.disposeAll(); - super.dispose(); - } -} diff --git a/packages/state_beacon/lib/src/extensions/extensions.dart b/packages/state_beacon/lib/src/extensions/extensions.dart deleted file mode 100644 index 55a21160..00000000 --- a/packages/state_beacon/lib/src/extensions/extensions.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:lite_ref/lite_ref.dart'; -import 'package:state_beacon/src/value_notifier_beacon.dart'; -import 'package:state_beacon_core/state_beacon_core.dart'; - -part 'readable.dart'; -part 'watch_observe.dart'; -part 'writable.dart'; -part 'scoped_ref.dart'; - -final Map> _vnCache = {}; - -@visibleForTesting - -/// The number of value notifiers currently in use -/// This is used for testing purposes only -bool hasNotifier(BaseBeacon beacon) { - return _vnCache.containsKey(beacon.hashCode); -} - -ValueNotifier _toValueNotifier(ReadableBeacon beacon) { - final key = beacon.hashCode; - - final existing = _vnCache[key]; - - if (existing != null) { - return existing as ValueNotifierBeacon; - } - - final notifier = ValueNotifierBeacon(beacon.peek()); - - _vnCache[key] = notifier; - - final unsub = beacon.subscribe(notifier.set, startNow: false); - - notifier.addDisposeCallback(() { - unsub(); - _vnCache.remove(key); - }); - - beacon.onDispose(() { - notifier.dispose(); - _vnCache.remove(key); - }); - - return notifier; -} diff --git a/packages/state_beacon/lib/src/extensions/readable.dart b/packages/state_beacon/lib/src/extensions/readable.dart deleted file mode 100644 index 56675153..00000000 --- a/packages/state_beacon/lib/src/extensions/readable.dart +++ /dev/null @@ -1,9 +0,0 @@ -part of 'extensions.dart'; - -// ignore: public_member_api_docs -extension ReadableBeaconFlutterUtils on ReadableBeacon { - /// Converts this to a [ValueListenable] - ValueListenable toListenable() { - return _toValueNotifier(this); - } -} diff --git a/packages/state_beacon/lib/src/extensions/scoped_ref.dart b/packages/state_beacon/lib/src/extensions/scoped_ref.dart index da73275b..f6e32d0b 100644 --- a/packages/state_beacon/lib/src/extensions/scoped_ref.dart +++ b/packages/state_beacon/lib/src/extensions/scoped_ref.dart @@ -1,4 +1,5 @@ -part of 'extensions.dart'; +import 'package:flutter/widgets.dart'; +import 'package:state_beacon/state_beacon.dart'; /// A function that takes a [BeaconController] and returns 1 of its beacon. typedef BeaconSelector = ReadableBeacon Function(C); diff --git a/packages/state_beacon/lib/src/extensions/value_notifier.dart b/packages/state_beacon/lib/src/extensions/value_notifier.dart deleted file mode 100644 index 1b5fc67a..00000000 --- a/packages/state_beacon/lib/src/extensions/value_notifier.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:state_beacon/state_beacon.dart'; - -/// Extensions for [ValueNotifier]. -extension ValueNotifierUtils on ValueNotifier { - /// Converts this to a [WritableBeacon]. - WritableBeacon toBeacon({BeaconGroup? group, String? name}) { - final beaconCreator = group ?? Beacon; - - final beacon = beaconCreator.writable(value, name: name); - - var syncing = false; - void safeWrite(VoidCallback fn) { - if (syncing) return; - syncing = true; - try { - fn(); - } finally { - syncing = false; - } - } - - void update() => safeWrite(() => beacon.set(value)); - - addListener(update); - - beacon - ..subscribe( - (v) => safeWrite(() => value = v), - synchronous: true, - ) - ..onDispose(() => removeListener(update)); - - return beacon; - } -} diff --git a/packages/state_beacon/lib/src/extensions/watch_observe.dart b/packages/state_beacon/lib/src/extensions/watch_observe.dart deleted file mode 100644 index 4084c911..00000000 --- a/packages/state_beacon/lib/src/extensions/watch_observe.dart +++ /dev/null @@ -1,132 +0,0 @@ -// ignore_for_file: invalid_use_of_protected_member, use_if_null_to_convert_nulls_to_bools, lines_longer_than_80_chars, deprecated_member_use - -part of 'extensions.dart'; - -// ignore: public_member_api_docs -typedef ObserverCallback = void Function(T prev, T next); - -// coverage:ignore-start -// requires a manual GC trigger to test -final Finalizer _finalizer = Finalizer((fn) => fn()); -// coverage:ignore-end - -/// @macro WidgetUtils -extension WidgetUtils on BaseBeacon { - /// Watches a beacon and triggers a widget - /// rebuild when its value changes. - /// - /// Note: must be called within a widget's build method. - /// - /// Usage: - /// ```dart - /// final counter = Beacon.writable(0); - /// - /// class Counter extends StatelessWidget { - /// const Counter({super.key}); - /// - /// @override - /// Widget build(BuildContext context) { - /// final count = counter.watch(context); - /// return Text(count.toString()); - /// } - ///} - /// ``` - T watch(BuildContext context) { - final key = context.hashCode; - - return _watchOrObserve( - key, - context, - ); - } - - /// Observes the state of a beacon and triggers a callback with the current state. - /// - /// The callback is provided with the current state of the beacon and a BuildContext. - /// This can be used to show snackbars or other side effects. - /// - /// Usage: - /// ```dart - /// final exampleBeacon = Beacon.writable("Initial State"); - /// - /// class ExampleWidget extends StatelessWidget { - /// @override - /// Widget build(BuildContext context) { - /// context.observe(exampleBeacon, (state, context) { - /// ScaffoldMessenger.of(context).showSnackBar( - /// SnackBar(content: Text(state)), - /// ); - /// }); - /// return Container(); - /// } - /// } - /// ``` - void observe( - BuildContext context, - ObserverCallback callback, { - bool synchronous = false, - }) { - final key = Object.hash( - context, - 'isObserving', // create 1 subscription for each widget - ); - - _watchOrObserve( - key, - context, - callback: () => callback(previousValue as T, peek()), - synchronous: synchronous, - ); - } - - T _watchOrObserve( - int key, - BuildContext context, { - VoidCallback? callback, - bool synchronous = false, - }) { - if ($$widgetSubscribers$$.contains(key)) { - return peek(); - } - - $$widgetSubscribers$$.add(key); - - final elementRef = WeakReference(context as Element); - late VoidCallback unsub; - - void rebuildWidget() { - elementRef.target!.markNeedsBuild(); - } - - final run = callback ?? rebuildWidget; - - void handleNewValue(T value) { - if (elementRef.target?.mounted == true) { - run(); - } else { - unsub(); - $$widgetSubscribers$$.remove(key); - } - } - - unsub = subscribe( - handleNewValue, - startNow: false, - synchronous: synchronous, - ); - - // coverage:ignore-start - // clean up if the widget is disposed - // and value is never modified again - _finalizer.attach( - context, - () { - unsub(); - $$widgetSubscribers$$.remove(key); - }, - ); - // coverage:ignore-end - - return peek(); - } -} diff --git a/packages/state_beacon/lib/src/extensions/writable.dart b/packages/state_beacon/lib/src/extensions/writable.dart deleted file mode 100644 index 56032b78..00000000 --- a/packages/state_beacon/lib/src/extensions/writable.dart +++ /dev/null @@ -1,13 +0,0 @@ -part of 'extensions.dart'; - -/// @macro [WritableBeaconFlutterUtils] -extension WritableBeaconFlutterUtils on WritableBeacon { - /// Converts this to a [ValueNotifier] - ValueNotifier toValueNotifier() { - final notifier = _toValueNotifier(this); - - notifier.addListener(() => set(notifier.value)); - - return notifier; - } -} diff --git a/packages/state_beacon/lib/src/notifier/text_editing_controller.dart b/packages/state_beacon/lib/src/notifier/text_editing_controller.dart deleted file mode 100644 index bb712edf..00000000 --- a/packages/state_beacon/lib/src/notifier/text_editing_controller.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:state_beacon/state_beacon.dart'; - -/// This is a wrapper around a [TextEditingController] that -/// allows you to hook into the controller's lifecycle. -class _TextEditingController extends TextEditingController { - VoidCallback? disposeCallback; - bool _disposed = false; - - @override - void dispose() { - if (_disposed) return; - _disposed = true; - disposeCallback?.call(); - super.dispose(); - } -} - -/// A beacon that wraps a [TextEditingController]. -class TextEditingBeacon extends WritableBeacon { - /// @macro [TextEditingBeacon] - TextEditingBeacon({String? text, BeaconGroup? group, super.name}) - : super( - initialValue: text == null - ? TextEditingValue.empty - : TextEditingValue(text: text), - ) { - group?.add(this); - var syncing = false; - - void safeWrite(VoidCallback fn) { - if (syncing) return; - syncing = true; - try { - fn(); - } finally { - syncing = false; - } - } - - _controller.addListener(() { - safeWrite(() => set(_controller.value, force: true)); - }); - - subscribe( - (v) => safeWrite(() => _controller.value = v), - synchronous: true, - ); - - _controller.disposeCallback = dispose; - } - - late final _controller = _TextEditingController(); - - /// The current [TextEditingController]. - TextEditingController get controller => _controller; - - /// The current string the user is editing. - String get text => _controller.text; - - set text(String newText) { - _controller.text = newText; - } - - /// The currently selected [text]. - /// - /// If the selection is collapsed, then this property gives - /// the offset of the cursor within the text. - TextSelection get selection => _controller.selection; - - /// Alias for controller.clear() - void clear() => _controller.clear(); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} diff --git a/packages/state_beacon/lib/src/scheduler.dart b/packages/state_beacon/lib/src/scheduler.dart deleted file mode 100644 index 68092cd2..00000000 --- a/packages/state_beacon/lib/src/scheduler.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/scheduler.dart'; -import 'package:state_beacon_core/state_beacon_core.dart' as core; - -/// `Effects` are not synchronous, their execution is controlled by a scheduler. -/// When a dependency of an `effect` changes, it is added to a queue and -/// the scheduler decides when is the best time to flush the queue. -/// By default, the queue is flushed with a DARTVM microtask which runs -/// on the next loop; this can be changed by setting a custom scheduler. -/// Flutter comes with its own scheduler, so it is recommended to use -/// flutter's scheduler when using beacons in a flutter app. -/// This can be done by calling `BeaconScheduler.useFlutterScheduler();` -/// in the `main` function. -/// -/// ```dart -/// void main() { -/// BeaconScheduler.useFlutterScheduler(); -/// -/// runApp(const MyApp()); -/// } -/// ``` -abstract class BeaconScheduler { - /// Runs all queued effects/subscriptions - /// This is made available for testing and should not be used in production - static void flush() => core.BeaconScheduler.flush(); - - /// This scheduler uses the Flutter SchedulerBinding to - /// schedule updates to be processed after the current frame. - static void useFlutterScheduler() { - _flushing = false; - core.BeaconScheduler.setScheduler(_flutterScheduler); - } - - /// This scheduler limits the frequency that updates - /// are processed to 60 times per second. - static void use60fpsScheduler() { - core.BeaconScheduler.use60fpsScheduler(); - } - - /// This scheduler limits the frequency that updates - /// are processed to a custom fps. - static void useCustomFpsScheduler(int updatesPerSecond) { - core.BeaconScheduler.useCustomFpsScheduler(updatesPerSecond); - } - - /// Sets the scheduler to the provided function - // coverage:ignore-start - static void setCustomScheduler(void Function() scheduler) { - core.BeaconScheduler.setScheduler(scheduler); - } - // coverage:ignore-end - - /// This scheduler processes updates synchronously. This is not recommended - /// for production apps and only provided to make testing easier. - /// - /// With this scheduler, you aren't protected from stackoverflows when - /// an effect mutates a beacon that it depends on. This is a infinite loop - /// with the sync scheduler. - // static void useSyncScheduler() { - // core.BeaconScheduler.useSyncScheduler(); - // } -} - -var _flushing = false; - -void _flutterScheduler() { - if (_flushing) return; - _flushing = true; - if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { - Future.microtask(() { - core.BeaconScheduler.flush(); - _flushing = false; - }); - } else { - // coverage:ignore-start - SchedulerBinding.instance.addPostFrameCallback((_) { - core.BeaconScheduler.flush(); - _flushing = false; - }); - // coverage:ignore-end - } -} diff --git a/packages/state_beacon/lib/src/value_notifier_beacon.dart b/packages/state_beacon/lib/src/value_notifier_beacon.dart deleted file mode 100644 index 707ab26e..00000000 --- a/packages/state_beacon/lib/src/value_notifier_beacon.dart +++ /dev/null @@ -1,26 +0,0 @@ -// ignore_for_file: public_member_api_docs, use_setters_to_change_properties - -import 'package:flutter/foundation.dart'; - -/// @macro [ValueNotifierBeacon] -class ValueNotifierBeacon extends ValueNotifier { - ValueNotifierBeacon(super.value); - - final disposeListeners = []; - - void addDisposeCallback(VoidCallback callback) { - disposeListeners.add(callback); - } - - void set(T newValue) { - value = newValue; - } - - @override - void dispose() { - for (final cb in disposeListeners) { - cb(); - } - super.dispose(); - } -} diff --git a/packages/state_beacon/lib/state_beacon.dart b/packages/state_beacon/lib/state_beacon.dart index e912253c..9e15a9c6 100644 --- a/packages/state_beacon/lib/state_beacon.dart +++ b/packages/state_beacon/lib/state_beacon.dart @@ -2,8 +2,5 @@ library; export 'package:lite_ref/lite_ref.dart'; -export 'package:state_beacon_core/state_beacon_core.dart' hide BeaconScheduler; -export 'src/controller/controller.dart'; -export 'src/extensions/extensions.dart' hide hasNotifier; -export 'src/notifier/text_editing_controller.dart'; -export 'src/scheduler.dart'; +export 'package:state_beacon_flutter/state_beacon_flutter.dart'; +export 'src/extensions/scoped_ref.dart'; diff --git a/packages/state_beacon/test/src/controller/controller_test.dart b/packages/state_beacon/test/src/controller/controller_test.dart deleted file mode 100644 index 594feac6..00000000 --- a/packages/state_beacon/test/src/controller/controller_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/state_beacon.dart'; - -void main() { - testWidgets('should dispose all beacons in State class', (tester) async { - await tester.pumpWidget(const CoounterView()); - final state = tester.state<_CoounterViewState>(find.byType(CoounterView)); - - expect(state.count.isDisposed, false); - expect(state.doubledCount.isDisposed, false); - - await tester.pumpWidget(const SizedBox.shrink()); - - expect(state.count.isDisposed, true); - expect(state.doubledCount.isDisposed, true); - }); -} - -class CoounterView extends StatefulWidget { - const CoounterView({super.key}); - - @override - State createState() => _CoounterViewState(); -} - -class _CoounterViewState extends State - with BeaconControllerMixin { - late final count = B.writable(0); - late final doubledCount = B.derived(() => count.value * 2); - - @override - Widget build(BuildContext context) { - return Container(); - } -} diff --git a/packages/state_beacon/test/src/edge_case_test.dart b/packages/state_beacon/test/src/edge_case_test.dart deleted file mode 100644 index e74a31f6..00000000 --- a/packages/state_beacon/test/src/edge_case_test.dart +++ /dev/null @@ -1,173 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/state_beacon.dart'; - -typedef Themes = ({ThemeData lightTheme}); - -class Customization { - const Customization({ - this.themes = const {}, - }); - - final Map themes; -} - -class PrefController { - PrefController({ - required this.defaultThemeName, - required this.appCustomization, - }); - - final String? defaultThemeName; - final Customization appCustomization; - - late final selectedTheme = Beacon.writable( - defaultThemeName ?? 'Default', - name: 'selectedTheme', - ); - - late final lightTheme = Beacon.derived( - () { - final selectedThemeName = selectedTheme.value; - return appCustomization.themes[selectedThemeName]?.lightTheme; - }, - name: 'lightTheme', - ); -} - -const color1 = Color(0xFF000000); -const color2 = Color(0xFFFF0000); -const colorFallback = Color(0xFFFFFFFF); - -const myWidgetKey = Key('myWidgetKey'); - -void main() { - testWidgets('Should change theme with empty', (widgetTester) async { - // BeaconObserver.instance = LoggingObserver(); - final controller = PrefController( - defaultThemeName: null, - appCustomization: Customization( - themes: { - '1': (lightTheme: ThemeData.light().copyWith(primaryColor: color1)), - '2': (lightTheme: ThemeData.light().copyWith(primaryColor: color2)), - }, - ), - ); - - await widgetTester.pumpWidget(MyApp(controller: controller)); - expect(controller.selectedTheme.peek(), 'Default'); - var byKey = find.byKey(myWidgetKey); - expect(byKey.first, findsOneWidget); - expect(widgetTester.widget(byKey).color, colorFallback); - - controller.selectedTheme.value = '2'; - await widgetTester.pumpAndSettle(); - expect(controller.selectedTheme.peek(), '2'); - byKey = find.byKey(myWidgetKey); - expect(byKey, findsOneWidget); - expect(widgetTester.widget(byKey).color, color2); - - controller.selectedTheme.value = '1'; - await widgetTester.pumpAndSettle(); - expect(controller.selectedTheme.peek(), '1'); - byKey = find.byKey(myWidgetKey); - expect(byKey, findsOneWidget); - expect(widgetTester.widget(byKey).color, color1); - }); - - testWidgets('Should change theme with invalid predefined value', - (widgetTester) async { - // BeaconObserver.instance = LoggingObserver(); - final controller = PrefController( - defaultThemeName: 'value not exists', - appCustomization: Customization( - themes: { - '1': (lightTheme: ThemeData.light().copyWith(primaryColor: color1)), - '2': (lightTheme: ThemeData.light().copyWith(primaryColor: color2)), - }, - ), - ); - - await widgetTester.pumpWidget(MyApp(controller: controller)); - expect(controller.selectedTheme.peek(), 'value not exists'); - var byKey = find.byKey(myWidgetKey); - expect(byKey.first, findsOneWidget); - expect(widgetTester.widget(byKey).color, colorFallback); - - controller.selectedTheme.value = '2'; - await widgetTester.pumpAndSettle(); - expect(controller.selectedTheme.peek(), '2'); - byKey = find.byKey(myWidgetKey); - expect(byKey, findsOneWidget); - expect(widgetTester.widget(byKey).color, color2); - - controller.selectedTheme.value = '1'; - await widgetTester.pumpAndSettle(); - expect(controller.selectedTheme.peek(), '1'); - byKey = find.byKey(myWidgetKey); - expect(byKey, findsOneWidget); - expect(widgetTester.widget(byKey).color, color1); - }); - - testWidgets('Should change theme with predefined value', - (widgetTester) async { - final controller = PrefController( - defaultThemeName: '1', - appCustomization: Customization( - themes: { - '1': (lightTheme: ThemeData.light().copyWith(primaryColor: color1)), - '2': (lightTheme: ThemeData.light().copyWith(primaryColor: color2)), - }, - ), - ); - - // BeaconObserver.useLogging(); - - await widgetTester.pumpWidget(MyApp(controller: controller)); - expect(controller.selectedTheme.peek(), '1'); - var byKey = find.byKey(myWidgetKey); - expect(byKey.first, findsOneWidget); - expect(widgetTester.widget(byKey).color, color1); - - controller.selectedTheme.value = '2'; - await widgetTester.pumpAndSettle(); - expect(controller.selectedTheme.peek(), '2'); - byKey = find.byKey(myWidgetKey); - expect(byKey, findsOneWidget); - expect(widgetTester.widget(byKey).color, color2); - - controller.selectedTheme.value = '1'; - await widgetTester.pumpAndSettle(); - expect(controller.selectedTheme.peek(), '1'); - byKey = find.byKey(myWidgetKey); - expect(byKey, findsOneWidget); - expect(widgetTester.widget(byKey).color, color1); - }); -} - -class MyApp extends StatelessWidget { - const MyApp({ - required this.controller, - super.key, - }); - - final PrefController controller; - - @override - Widget build(BuildContext context) { - return MaterialApp( - theme: controller.lightTheme.watch(context) ?? - ThemeData.light().copyWith(primaryColor: colorFallback), - home: Builder( - builder: (ctx) { - return Scaffold( - body: ColoredBox( - key: myWidgetKey, - color: Theme.of(ctx).primaryColor, - ), - ); - }, - ), - ); - } -} diff --git a/packages/state_beacon/test/src/extensions/readable_test.dart b/packages/state_beacon/test/src/extensions/readable_test.dart deleted file mode 100644 index e1298f53..00000000 --- a/packages/state_beacon/test/src/extensions/readable_test.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/src/extensions/extensions.dart'; -import 'package:state_beacon/state_beacon.dart'; - -void main() { - test('should convert to a value listenable', () { - final beacon = Beacon.writable(0); - - final valueNotifier = beacon.toListenable(); - - expect(valueNotifier, isA>()); - - var called = 0; - - int fn() => called++; - valueNotifier.addListener(fn); - - beacon.value = 1; - - BeaconScheduler.flush(); - - expect(called, 1); - - beacon.value = 2; - - BeaconScheduler.flush(); - - expect(called, 2); - - valueNotifier.removeListener(fn); - - beacon.value = 3; - - BeaconScheduler.flush(); - - expect(called, 2); - }); - - test('should return the same listener instance', () { - final beacon = Beacon.writable(0); - - final valueNotifier = beacon.toListenable(); - final valueNotifier2 = beacon.toListenable(); - final valueNotifier3 = beacon.toListenable(); - - expect(valueNotifier, valueNotifier2); - expect(valueNotifier2, valueNotifier3); - - expect(hasNotifier(beacon), isTrue); - - beacon.dispose(); - - expect(hasNotifier(beacon), isFalse); - }); - - test('should remove notifier from cache when source is disposed.', () { - final age = Beacon.writable(50); - final name = Beacon.writable('bob'); - - age.toListenable(); - name.toListenable(); - - expect(hasNotifier(age), isTrue); - expect(hasNotifier(name), isTrue); - - age.dispose(); - - expect(hasNotifier(age), isFalse); - expect(hasNotifier(name), isTrue); - - name.dispose(); - - expect(hasNotifier(age), isFalse); - expect(hasNotifier(name), isFalse); - }); -} diff --git a/packages/state_beacon/test/src/extensions/value_notifier_test.dart b/packages/state_beacon/test/src/extensions/value_notifier_test.dart deleted file mode 100644 index 05c66989..00000000 --- a/packages/state_beacon/test/src/extensions/value_notifier_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/src/extensions/value_notifier.dart'; -import 'package:state_beacon/state_beacon.dart'; - -void main() { - test('should be synced with source value notifier', () async { - final notifier = ValueNotifier(0); - final beacon = notifier.toBeacon(); - - expect(beacon.peek(), 0); - - notifier.value = 1; - - expect(beacon.value, 1); - - notifier.value = 2; - - expect(beacon.value, 2); - - beacon.increment(); - - expect(beacon.value, 3); - - expect(notifier.value, 3); - }); -} diff --git a/packages/state_beacon/test/src/extensions/writable_test.dart b/packages/state_beacon/test/src/extensions/writable_test.dart deleted file mode 100644 index 6211deaa..00000000 --- a/packages/state_beacon/test/src/extensions/writable_test.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/src/extensions/extensions.dart'; -import 'package:state_beacon/state_beacon.dart'; - -void main() { - test('should convert to a value notifier', () { - final beacon = Beacon.writable(0); - - final valueNotifier = beacon.toValueNotifier(); - - expect(valueNotifier, isA>()); - - var called = 0; - - valueNotifier.addListener(() => called++); - - beacon.value = 1; - - BeaconScheduler.flush(); - - expect(called, 1); - - beacon.value = 2; - - BeaconScheduler.flush(); - - expect(called, 2); - - valueNotifier.dispose(); - - beacon.value = 3; - - BeaconScheduler.flush(); - - expect(called, 2); - }); - - test('should remove listeners source beacon is disposed', () { - BeaconScheduler.use60fpsScheduler(); // just to test it out for coverage - - final beacon = Beacon.writable(0); - - final valueNotifier = beacon.toValueNotifier(); - - expect(valueNotifier, isA>()); - - var called = 0; - - valueNotifier.addListener(() => called++); - - beacon.value = 1; - - BeaconScheduler.flush(); - - expect(called, 1); - - beacon.value = 2; - - BeaconScheduler.flush(); - - expect(called, 2); - - beacon.dispose(); - - // ignore: invalid_use_of_protected_member - expect(valueNotifier.hasListeners, false); - }); - - test('should return the same notifier instance', () { - final beacon = Beacon.writable(0); - - final valueNotifier = beacon.toValueNotifier(); - final valueNotifier2 = beacon.toValueNotifier(); - final valueNotifier3 = beacon.toValueNotifier(); - - expect(valueNotifier, valueNotifier2); - expect(valueNotifier2, valueNotifier3); - - expect(hasNotifier(beacon), isTrue); - - beacon.dispose(); - - expect(hasNotifier(beacon), isFalse); - }); - - test('should remove notifier from cache when notifier is disposed.', () { - final age = Beacon.writable(50); - final name = Beacon.writable('bob'); - - final aNotifier = age.toValueNotifier(); - final nNotifier = name.toValueNotifier(); - - expect(hasNotifier(age), isTrue); - expect(hasNotifier(name), isTrue); - expect(age.listenersCount, 1); - expect(name.listenersCount, 1); - - aNotifier.dispose(); - - expect(hasNotifier(age), isFalse); - expect(hasNotifier(name), isTrue); - expect(age.listenersCount, 0); - - nNotifier.dispose(); - - expect(hasNotifier(age), isFalse); - expect(hasNotifier(name), isFalse); - expect(name.listenersCount, 0); - }); -} diff --git a/packages/state_beacon/test/src/flutter_test.dart b/packages/state_beacon/test/src/flutter_test.dart deleted file mode 100644 index a20a9715..00000000 --- a/packages/state_beacon/test/src/flutter_test.dart +++ /dev/null @@ -1,298 +0,0 @@ -// ignore_for_file: cascade_invocations - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/state_beacon.dart'; - -import '../common.dart'; - -void main() { - BeaconScheduler.useFlutterScheduler(); - testWidgets('should rebuild Counter widget when count changes', - (WidgetTester tester) async { - final counter = Beacon.writable(0); - - final widget = Counter(counter: counter); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: widget, - ), - ), - ); - - expect(find.text('0'), findsOneWidget); - - counter.increment(); - - await tester.pumpAndSettle(); - - // Verify updated state - expect(find.text('1'), findsOneWidget); - expect(widget.builtCount, 2); - }); - - testWidgets('should rebuild FutureCounter on state changes', - (WidgetTester tester) async { - BeaconScheduler.useCustomFpsScheduler(60); - // BeaconObserver.instance = LoggingObserver(); - final counter = Beacon.writable(0, name: 'counter'); - - final derivedFutureCounter = Beacon.future( - () async { - final count = counter.value; - return counterFuture(count); - }, - name: 'derived', - ); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: FutureCounter(derived: derivedFutureCounter), - ), - ), - k10ms, - ); - - await tester.pumpAndSettle(); - - expect(find.text('${counter.value} second has passed.'), findsOneWidget); - - counter.increment(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: FutureCounter(derived: derivedFutureCounter), - ), - ), - k10ms * 2, - ); - - await tester.pumpAndSettle(); - - // Verify loading indicator - expect(find.text('${counter.value} second has passed.'), findsOneWidget); - - counter.value = 5; - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: FutureCounter(derived: derivedFutureCounter), - ), - ), - k10ms * 2, - ); - - await tester.pumpAndSettle(); - - expect( - find.text('Exception: Count(${counter.value}) too large'), - findsOneWidget, - ); - }); - - testWidgets('should show snackbar for exceeding 3', - (WidgetTester tester) async { - // Build the Counter widget - final counter = Beacon.writable(0); - final widget = Counter(counter: counter); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: widget, - ), - ), - ); - - // Increase counter beyond limit - counter.value = 4; - await tester.pumpAndSettle(); - - // Verify snackbar visibility - expect(find.byType(SnackBar), findsOneWidget); - expect(find.text('Count cannot be greater than 3'), findsOneWidget); - expect(widget.observedCount, 1); - }); - - testWidgets('should batch calls observe ', (WidgetTester tester) async { - final counter = Beacon.writable(0); - final widget = Counter(counter: counter); - await tester.pumpWidget(MaterialApp(home: Scaffold(body: widget))); - - counter.increment(); - counter.increment(); - counter.increment(); - - await tester.pumpAndSettle(); - - expect(widget.observedCount, 1); - }); - - testWidgets('should call observe synchronously', (WidgetTester tester) async { - final counter = Beacon.writable(0); - final widget = Counter(counter: counter, synchronous: true); - await tester.pumpWidget(MaterialApp(home: Scaffold(body: widget))); - - counter.increment(); - counter.increment(); - counter.increment(); - - await tester.pumpAndSettle(); - - expect(widget.observedCount, 3); - }); - - testWidgets('should show snackbar for going negative', - (WidgetTester tester) async { - // Build the Counter widget - final counter = Beacon.writable(0); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Counter(counter: counter), - ), - ), - ); - - // Decrease counter below limit - counter.value = -1; - await tester.pumpAndSettle(); - - // Verify snackbar visibility - expect(find.byType(SnackBar), findsOneWidget); - expect(find.text('Count cannot be negative'), findsOneWidget); - }); - - testWidgets('listenersCount decreases on widget dispose', - (WidgetTester tester) async { - // BeaconObserver.instance = LoggingObserver(); - final testCounter = Beacon.writable(0, name: 'testCounter'); - // BeaconScheduler.setScheduler(flutterScheduler); - - // Check initial listeners count - expect(testCounter.listenersCount, 0); - - // Build the 5 Counter widget, each with 2 listeners - // 1 for the text and 1 for the observer - await tester.pumpWidget( - MaterialApp( - home: Column( - children: [ - Counter(counter: testCounter), - Counter(counter: testCounter), - Counter(counter: testCounter), - Counter(counter: testCounter), - Counter(counter: testCounter), - ], - ), - ), - ); - - // Check listeners count after widget is built - expect(testCounter.listenersCount, 10); - - // Dispose the Counter widget by pumping a different widget - await tester.pumpWidget(const MaterialApp(home: SizedBox())); - - testCounter.value = 1; - - await tester.pumpAndSettle(); - - // Check listeners count after widget is disposed - expect(testCounter.listenersCount, 0); - - testCounter.dispose(); - }); -} - -Future counterFuture(int count) async { - if (count > 3) { - throw Exception('Count($count) too large'); - } - - await Future.delayed(Duration(milliseconds: count * 10)); - return '$count second has passed.'; -} - -class CounterColumn extends StatelessWidget { - const CounterColumn({required this.counter, required this.show, super.key}); - - final WritableBeacon counter; - final WritableBeacon show; - - @override - Widget build(BuildContext context) { - return Column( - children: show.watch(context) - ? [ - Counter(counter: counter), - Counter(counter: counter), - Counter(counter: counter), - Counter(counter: counter), - Counter(counter: counter), - ] - : [Container()], - ); - } -} - -// ignore: must_be_immutable -class Counter extends StatelessWidget { - Counter({required this.counter, super.key, this.synchronous = false}); - - final bool synchronous; - final WritableBeacon counter; - - int builtCount = 0; - int observedCount = 0; - - @override - Widget build(BuildContext context) { - builtCount++; - counter.observe( - context, - (prev, next) { - observedCount++; - final messenger = ScaffoldMessenger.of(context); - messenger.clearSnackBars(); - if (next > prev && next > 3) { - messenger.showSnackBar( - const SnackBar( - content: Text('Count cannot be greater than 3'), - ), - ); - } else if (next < prev && next < 0) { - messenger.showSnackBar( - const SnackBar( - content: Text('Count cannot be negative'), - ), - ); - } - }, - synchronous: synchronous, - ); - return Text( - counter.watch(context).toString(), - style: Theme.of(context).textTheme.headlineMedium, - ); - } -} - -class FutureCounter extends StatelessWidget { - const FutureCounter({required this.derived, super.key}); - - final FutureBeacon derived; - - @override - Widget build(BuildContext context) { - return switch (derived.watch(context)) { - AsyncData(value: final v) => Text(v), - AsyncError(error: final e) => Text('$e'), - _ => const CircularProgressIndicator(), - }; - } -} diff --git a/packages/state_beacon/test/src/notifier/text_editing_controller_test.dart b/packages/state_beacon/test/src/notifier/text_editing_controller_test.dart deleted file mode 100644 index 6f74318a..00000000 --- a/packages/state_beacon/test/src/notifier/text_editing_controller_test.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/state_beacon.dart'; - -void main() { - test('should be synced with internal TextEditingController', () async { - final beacon = TextEditingBeacon(); - final controller = beacon.controller; - - expect(beacon.peek(), TextEditingValue.empty); - expect(beacon.text, ''); - expect(controller.text, ''); - - beacon.text = '1'; - - expect(beacon.text, '1'); - expect(controller.text, '1'); - - beacon.text = '2'; - - expect(beacon.text, '2'); - expect(controller.text, '2'); - - beacon.clear(); - - expect(beacon.text, ''); - expect(controller.text, ''); - - controller.text = '3'; - - expect(beacon.text, '3'); - expect(controller.text, '3'); - - controller.clear(); - - expect(beacon.text, ''); - expect(controller.text, ''); - - controller.dispose(); - - expect(beacon.isDisposed, true); - }); - - test('should reflect changes to the controller', () async { - final beacon = TextEditingBeacon(text: '1'); - final controller = beacon.controller; - - expect(beacon.text, '1'); - expect(controller.text, '1'); - - expect(beacon.selection, controller.selection); - }); - - test('should add beacon to group provided', () { - final group = BeaconGroup(); - final beacon = TextEditingBeacon(text: '1', group: group); - - expect(group.beacons.length, 1); - expect(group.beacons.first, beacon); - - group.disposeAll(); - - expect(group.beacons.isEmpty, true); - - expect(beacon.isDisposed, true); - }); -} diff --git a/packages/state_beacon/test/src/value_notifier_beacon_test.dart b/packages/state_beacon/test/src/value_notifier_beacon_test.dart deleted file mode 100644 index 78508f6a..00000000 --- a/packages/state_beacon/test/src/value_notifier_beacon_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// ignore_for_file: invalid_use_of_protected_member - -import 'package:flutter_test/flutter_test.dart'; -import 'package:state_beacon/src/value_notifier_beacon.dart'; - -void main() { - test('should notify listener and call dispose callbacks', () { - final beacon = ValueNotifierBeacon(0); - var called = 0; - - void disposeTest() => called++; - - beacon.addListener(() => called++); - - expect(beacon.value, 0); - - beacon.set(1); - - expect(beacon.value, 1); - expect(called, 1); - - beacon - ..addDisposeCallback(disposeTest) - ..dispose(); - - expect(called, 2); - }); -} From 78a1bbec7303e9dfb067120ab1ea15f776c58928 Mon Sep 17 00:00:00 2001 From: jinyus Date: Thu, 30 May 2024 08:20:09 -0500 Subject: [PATCH 3/4] v1.0.0 --- examples/auth_flow/pubspec.lock | 14 +++++++++++--- examples/counter/pubspec.lock | 14 +++++++++++--- examples/flutter_main/pubspec.lock | 14 +++++++++++--- examples/github_search/pubspec.lock | 14 +++++++++++--- examples/shopping_cart/pubspec.lock | 14 +++++++++++--- examples/skeleton/pubspec.lock | 14 +++++++++++--- examples/vgv_best_practices/pubspec.lock | 14 +++++++++++--- packages/state_beacon/CHANGELOG.md | 4 ++++ packages/state_beacon/pubspec.yaml | 4 ++-- packages/state_beacon_core/CHANGELOG.md | 4 ++++ packages/state_beacon_core/pubspec.yaml | 2 +- 11 files changed, 88 insertions(+), 24 deletions(-) diff --git a/examples/auth_flow/pubspec.lock b/examples/auth_flow/pubspec.lock index ba836def..9c3a357d 100644 --- a/examples/auth_flow/pubspec.lock +++ b/examples/auth_flow/pubspec.lock @@ -211,15 +211,23 @@ packages: path: "../../packages/state_beacon" relative: true source: path - version: "0.45.0" + version: "1.0.0" state_beacon_core: dependency: transitive description: name: state_beacon_core - sha256: "7e6b3f157cb822acb77829f6696e562064a4b9a5cd687e4056d203288135c20c" + sha256: c109a5fee4b93f1cf2fcb2e6ea9c74285654e3e8799eb6e33c69581807214f94 url: "https://pub.dev" source: hosted - version: "0.43.5" + version: "1.0.0" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "4429e1efc43ded8d05d34e2d9049cefa5c4d48a1f3cdaa43435015c5d73ec284" + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: diff --git a/examples/counter/pubspec.lock b/examples/counter/pubspec.lock index 952c2430..9855d332 100644 --- a/examples/counter/pubspec.lock +++ b/examples/counter/pubspec.lock @@ -174,15 +174,23 @@ packages: path: "../../packages/state_beacon" relative: true source: path - version: "0.45.0" + version: "1.0.0" state_beacon_core: dependency: transitive description: name: state_beacon_core - sha256: "7e6b3f157cb822acb77829f6696e562064a4b9a5cd687e4056d203288135c20c" + sha256: c109a5fee4b93f1cf2fcb2e6ea9c74285654e3e8799eb6e33c69581807214f94 url: "https://pub.dev" source: hosted - version: "0.43.5" + version: "1.0.0" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "4429e1efc43ded8d05d34e2d9049cefa5c4d48a1f3cdaa43435015c5d73ec284" + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: diff --git a/examples/flutter_main/pubspec.lock b/examples/flutter_main/pubspec.lock index e465c279..2e56e771 100644 --- a/examples/flutter_main/pubspec.lock +++ b/examples/flutter_main/pubspec.lock @@ -398,15 +398,23 @@ packages: path: "../../packages/state_beacon" relative: true source: path - version: "0.45.0" + version: "1.0.0" state_beacon_core: dependency: transitive description: name: state_beacon_core - sha256: "7e6b3f157cb822acb77829f6696e562064a4b9a5cd687e4056d203288135c20c" + sha256: c109a5fee4b93f1cf2fcb2e6ea9c74285654e3e8799eb6e33c69581807214f94 url: "https://pub.dev" source: hosted - version: "0.43.5" + version: "1.0.0" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "4429e1efc43ded8d05d34e2d9049cefa5c4d48a1f3cdaa43435015c5d73ec284" + url: "https://pub.dev" + source: hosted + version: "1.0.0" state_beacon_lint: dependency: "direct dev" description: diff --git a/examples/github_search/pubspec.lock b/examples/github_search/pubspec.lock index 7af2c150..d3fcbba9 100644 --- a/examples/github_search/pubspec.lock +++ b/examples/github_search/pubspec.lock @@ -203,15 +203,23 @@ packages: path: "../../packages/state_beacon" relative: true source: path - version: "0.45.1" + version: "1.0.0" state_beacon_core: dependency: transitive description: name: state_beacon_core - sha256: "7e6b3f157cb822acb77829f6696e562064a4b9a5cd687e4056d203288135c20c" + sha256: c109a5fee4b93f1cf2fcb2e6ea9c74285654e3e8799eb6e33c69581807214f94 url: "https://pub.dev" source: hosted - version: "0.43.5" + version: "1.0.0" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "4429e1efc43ded8d05d34e2d9049cefa5c4d48a1f3cdaa43435015c5d73ec284" + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: diff --git a/examples/shopping_cart/pubspec.lock b/examples/shopping_cart/pubspec.lock index 17605bce..11c6f21d 100644 --- a/examples/shopping_cart/pubspec.lock +++ b/examples/shopping_cart/pubspec.lock @@ -211,15 +211,23 @@ packages: path: "../../packages/state_beacon" relative: true source: path - version: "0.45.0" + version: "1.0.0" state_beacon_core: dependency: transitive description: name: state_beacon_core - sha256: "7e6b3f157cb822acb77829f6696e562064a4b9a5cd687e4056d203288135c20c" + sha256: c109a5fee4b93f1cf2fcb2e6ea9c74285654e3e8799eb6e33c69581807214f94 url: "https://pub.dev" source: hosted - version: "0.43.5" + version: "1.0.0" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "4429e1efc43ded8d05d34e2d9049cefa5c4d48a1f3cdaa43435015c5d73ec284" + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: diff --git a/examples/skeleton/pubspec.lock b/examples/skeleton/pubspec.lock index fa218aeb..11780f65 100644 --- a/examples/skeleton/pubspec.lock +++ b/examples/skeleton/pubspec.lock @@ -195,15 +195,23 @@ packages: path: "../../packages/state_beacon" relative: true source: path - version: "0.45.0" + version: "1.0.0" state_beacon_core: dependency: transitive description: name: state_beacon_core - sha256: "7e6b3f157cb822acb77829f6696e562064a4b9a5cd687e4056d203288135c20c" + sha256: c109a5fee4b93f1cf2fcb2e6ea9c74285654e3e8799eb6e33c69581807214f94 url: "https://pub.dev" source: hosted - version: "0.43.5" + version: "1.0.0" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "4429e1efc43ded8d05d34e2d9049cefa5c4d48a1f3cdaa43435015c5d73ec284" + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: diff --git a/examples/vgv_best_practices/pubspec.lock b/examples/vgv_best_practices/pubspec.lock index a74823e0..dae06e89 100644 --- a/examples/vgv_best_practices/pubspec.lock +++ b/examples/vgv_best_practices/pubspec.lock @@ -203,15 +203,23 @@ packages: path: "../../packages/state_beacon" relative: true source: path - version: "0.45.0" + version: "1.0.0" state_beacon_core: dependency: transitive description: name: state_beacon_core - sha256: "7e6b3f157cb822acb77829f6696e562064a4b9a5cd687e4056d203288135c20c" + sha256: c109a5fee4b93f1cf2fcb2e6ea9c74285654e3e8799eb6e33c69581807214f94 url: "https://pub.dev" source: hosted - version: "0.43.5" + version: "1.0.0" + state_beacon_flutter: + dependency: transitive + description: + name: state_beacon_flutter + sha256: "4429e1efc43ded8d05d34e2d9049cefa5c4d48a1f3cdaa43435015c5d73ec284" + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: diff --git a/packages/state_beacon/CHANGELOG.md b/packages/state_beacon/CHANGELOG.md index 3bfe95d1..9072c43b 100644 --- a/packages/state_beacon/CHANGELOG.md +++ b/packages/state_beacon/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.0 + +- Stable release + # 0.45.2 - [Fix] Edge case for Subscriptions diff --git a/packages/state_beacon/pubspec.yaml b/packages/state_beacon/pubspec.yaml index 97cf9aa5..0be221de 100644 --- a/packages/state_beacon/pubspec.yaml +++ b/packages/state_beacon/pubspec.yaml @@ -1,6 +1,6 @@ name: state_beacon description: A reactive primitive and simple state managerment solution for dart and flutter -version: 0.45.2 +version: 1.0.0 repository: https://github.com/jinyus/dart_beacon environment: @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter lite_ref: ^0.8.1 - state_beacon_core: ^0.43.6 + state_beacon_flutter: ^1.0.0 dev_dependencies: flutter_lints: ^3.0.0 diff --git a/packages/state_beacon_core/CHANGELOG.md b/packages/state_beacon_core/CHANGELOG.md index 15f3d85e..4a5a4d26 100644 --- a/packages/state_beacon_core/CHANGELOG.md +++ b/packages/state_beacon_core/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.0 + +- Stable release + # 0.43.6 - [Fix] Edge case for Subscriptions diff --git a/packages/state_beacon_core/pubspec.yaml b/packages/state_beacon_core/pubspec.yaml index 7108f1a6..926e80be 100644 --- a/packages/state_beacon_core/pubspec.yaml +++ b/packages/state_beacon_core/pubspec.yaml @@ -1,6 +1,6 @@ name: state_beacon_core description: A reactive primitive and simple state managerment solution for dart. -version: 0.43.6 +version: 1.0.0 repository: https://github.com/jinyus/dart_beacon environment: From 7a2d7d1e58b6811ae06426f8145375d2dfee6629 Mon Sep 17 00:00:00 2001 From: jinyus Date: Thu, 30 May 2024 08:24:30 -0500 Subject: [PATCH 4/4] chore: Refactor Beacon.family tests to remove unnecessary cache option --- .../test/src/beacons/family_test.dart | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/state_beacon_core/test/src/beacons/family_test.dart b/packages/state_beacon_core/test/src/beacons/family_test.dart index 8aefcc15..e7163ac4 100644 --- a/packages/state_beacon_core/test/src/beacons/family_test.dart +++ b/packages/state_beacon_core/test/src/beacons/family_test.dart @@ -83,10 +83,7 @@ void main() { }); test('should remove from cache when disposed', () { - final family = Beacon.family( - (int arg) => Beacon.writable('$arg'), - cache: true, - ); + final family = Beacon.family((int arg) => Beacon.writable('$arg')); final beacon1 = family(1); beacon1.dispose(); @@ -97,10 +94,7 @@ void main() { }); test('should clear the cache and dispose beacons', () { - final family = Beacon.family( - (int arg) => Beacon.writable('$arg'), - cache: true, - ); + final family = Beacon.family((int arg) => Beacon.writable('$arg')); final beacon1 = family(1); @@ -114,10 +108,7 @@ void main() { }); test('should not clear beacons individually when clearing', () { - final family = Beacon.family( - (int arg) => Beacon.writable('$arg'), - cache: true, - ); + final family = Beacon.family((int arg) => Beacon.writable('$arg')); var ran = 0;