state_beacon

Creator: coderz1093

Last updated:

0 purchases

TODO
Add to Cart

Description:

state beacon

Overview #
A Beacon is a reactive primitive(signal) and simple state management solution for Dart and Flutter; state_beacon leverages the node coloring technique created by Milo Mighdoll and used in the latest versions of SolidJS and reactively.
Flutter web demo(source): https://flutter-beacon.surge.sh/
All examples: https://github.com/jinyus/dart_beacon/tree/main/examples



Installation #
dart pub add state_beacon
copied to clipboard
Usage #
import 'package:flutter/material.dart';
import 'package:state_beacon/state_beacon.dart';

final name = Beacon.writable("Bob");

class ProfileCard extends StatelessWidget {
const ProfileCard({super.key});

@override
Widget build(BuildContext context) {
// rebuilds whenever the name changes
return Text(name.watch(context));
}
}
copied to clipboard
Using an asynchronous function
final counter = Beacon.writable(0);

// The future will be recomputed whenever the counter changes
final futureCounter = Beacon.future(() async {
final count = counter.value;
return await fetchData(count);
});

Future<String> fetchData(int count) async {
await Future.delayed(Duration(seconds: count));
return '$count second has passed.';
}

class FutureCounter extends StatelessWidget {
const FutureCounter({super.key});

@override
Widget build(BuildContext context) {
return switch (futureCounter.watch(context)) {
AsyncData<String>(value: final v) => Text(v),
AsyncError(error: final e) => Text('$e'),
_ => const CircularProgressIndicator(),
};
}
}
copied to clipboard
Linting (optional) #
It is recommended to use state_beacon_lint for package specific rules.
dart pub add custom_lint state_beacon_lint --dev
copied to clipboard
Enable the custom_lint plugin in your analysis_options.yaml file by adding the following.
analyzer:
plugins:
- custom_lint
copied to clipboard
NB: Create the file if it doesn't exist.
Features #

Beacon.writable: Mutable beacon that allows both reading and writing.
Beacon.readable: Immutable beacon that only emit values, ideal for readonly data.
Beacon.derived: Derive values from other beacons, keeping them reactively in sync.
Beacon.effect: React to changes in beacon values.
BeaconScheduler: Configure the scheduler for all beacons.
Beacon.future: Derive values from asynchronous operations, managing state during computation.

Properties
Methods


Beacon.stream: Create derived beacons from Dart streams. values are wrapped in an AsyncValue.
Beacon.streamRaw: Like Beacon.stream, but it doesn't wrap the value in an AsyncValue.
Beacon.debounced: Delay value updates until a specified time has elapsed, preventing rapid or unwanted updates.
Beacon.throttled: Limit the frequency of value updates, ideal for managing frequent events or user input.
Beacon.filtered: Update values based on filter criteria.
Beacon.timestamped: Attach timestamps to each value update.
Beacon.undoRedo: Provides the ability to undo and redo value changes.
Beacon.bufferedCount: Create a buffer/list of values based an int limit.
Beacon.bufferedTime: Create a buffer/list of values based on a time limit.
Beacon.list: Manage reactive lists that automatically update dependent beacons upon changes.

Beacon.hashSet
Beacon.hashMap


AsyncValue: A wrapper around a value that can be in one of four states: idle, loading, data, or error.

unwrap: Casts this [AsyncValue] to [AsyncData] and return its value.
lastData: Returns the latest valid data value or null.
tryCatch: Execute a future and return [AsyncData] or [AsyncError].
optimistic updates: Update the value optimistically when using tryCatch.


Beacon.family: Create and manage a family of related beacons.
BeaconGroup: Create, reset and dispose and group of beacons.
Methods: Additional methods for beacons that can be chained.

subscribe()
tostream()
wrap()
ingest()
next()
toListenable()
toValueNotifier()
dispose()
onDispose()


Chaining Beacons: Seamlessly chain beacons to create sophisticated reactive pipelines, combining multiple functionalities for advanced value manipulation and control.

buffer
bufferTime
throttle
filter
map
debounce


Debugging
Disposal
Testing
BeaconController
Dependency Injection

Pitfalls
Beacon.writable: #
A WritableBeacon is a mutable reactive value that notifies listeners when its value changes. You might think it's just a ValueNotifier, but the power in beacons/signals is their composability.
final counter = Beacon.writable(0);
counter.value = 10;
print(counter.value); // 10
copied to clipboard
Beacon.lazyWritable: #
Like Beacon.writable but behaves like a late variable. It must be set before it's read.
NB: All writable beacons have a lazy counterpart.
final counter = Beacon.lazyWritable();

print(counter.value); // throws UninitializeLazyReadException()

counter.value = 10;
print(counter.value); // 10
copied to clipboard
Beacon.readable: #
This is useful for exposing a WritableBeacon's value to consumers without allowing them to modify it. This is the superclass of all beacons.
final _internalCounter = Beacon.writable(10);

// Expose the beacon's value without allowing it to be modified
ReadableBeacon<int> get counter => _internalCounter;
copied to clipboard
Beacon.derived: #
A DerivedBeacon is composed of other beacons. It automatically tracks any beacons accessed within its closure and will recompute its value when one of them changes.
These beacons are lazy and will only compute their value when accessed, subscribed to or being watched by a widget or an effect.
Example:
final age = Beacon.writable(18);
final canDrink = Beacon.derived(() => age.value >= 21);

canDrink.subscribe((value) {
print(value); // Outputs: false
});

// Outputs: false

age.value = 22;
// the derived beacon will be updated and the subscribers are notified

// Outputs: true
copied to clipboard
Beacon.effect: #
An effect is just a function that will re-run whenever one of its
dependencies change.
Any beacon accessed within the effect will be tracked as a dependency. A change to the value of any of the tracked beacons will trigger the effect to re-run.
An effect is scheduled to run immediately after creation.
final age = Beacon.writable(15);

// this effect runs immediately and whenever age changes
Beacon.effect(() {
if (age.value >= 18) {
print("You can vote!");
} else {
print("You can't vote yet");
}
});

// Outputs: "You can't vote yet"

age.value = 20; // Outputs: "You can vote!"
copied to clipboard
BeaconScheduler: #
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.
A 60fps scheduler is included, this limits processing effects to 60 times per second. This can be done by calling BeaconScheduler.use60FpsScheduler(); in the main function. You can also create your own custom scheduler for more advanced use cases. eg: Gaming: Synchronize flushing with your game loop.
When testing synchronous code, it is necessary to flush the queue manually. This can be done by calling BeaconScheduler.flush(); in your test.

Note
When writing widget tests, manual flushing isn't needed. The queue is automatically flushed when you call tester.pumpAndSettle().

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);
copied to clipboard
Beacon.future: #
Creates a FutureBeacon whose value is derived from an asynchronous computation.
This beacon will recompute its value every time one of its dependencies change.
The result is wrapped in an AsyncValue, which can be in one of four states: idle, loading, data, or error.
If manualStart is true (default: false), the beacon will be in the idle state and the future will not execute until start() is called. Calling start() on a beacon that's already started will have no effect.
If shouldSleep is true(default), the callback will not execute if the beacon is no longer being watched.
It will resume executing once a listener is added or its value is accessed.
This means that it will enter the loading state when woken up.
NB: You can access the last successful data while the beacon is in the loading or error state using myFutureBeacon.lastData. Calling lastdata while in the data state will return the current value.

Important
Only beacons accessed before the async gap will be tracked as dependencies. See pitfalls for more details.

Example:
final counter = Beacon.writable(0);

// The future will be recomputed whenever the counter changes
final futureCounter = Beacon.future(() async {
final count = counter.value;
await Future.delayed(Duration(seconds: count));
return '$count second has passed.';
});

class FutureCounter extends StatelessWidget {
const FutureCounter({super.key});

@override
Widget build(BuildContext context) {
return switch (futureCounter.watch(context)) {
AsyncData<String>(value: final v) => Text(v),
AsyncError(error: final e) => Text('$e'),
AsyncLoading() || AsyncIdle() => const CircularProgressIndicator(),
};
}
}
copied to clipboard
Can be transformed into a future with myFutureBeacon.toFuture()
This can useful when a FutureBeacon depends on another FutureBeacon.
This functionality is also available to StreamBeacons.
var count = Beacon.writable(0);

var firstName = Beacon.future(() async {
final val = count.value;
await Future.delayed(k10ms);
return 'Sally $val';
});

var lastName = Beacon.future(() async {
final val = count.value + 1;
await Future.delayed(k10ms);
return 'Smith $val';
});

var fullName = Beacon.future(() async {
// wait for the future to complete
// we don't have to manually handle all the states
final [fname, lname] = await Future.wait(
[
firstName.toFuture(),
lastName.toFuture(),
],
);

return '$fname $lname';
});
copied to clipboard
FutureBeacon.overrideWith:
Replaces the current callback and resets the beacon by running the new callback.
var futureBeacon = Beacon.future(() async => 1);

await Future.delayed(k1ms);

expect(futureBeacon.unwrapValue(), 1);

futureBeacon.overrideWith(() async => throw Exception('error'));

await Future.delayed(k1ms);

expect(futureBeacon.isError, true);
copied to clipboard
Properties:
All these methods are also available to StreamBeacons.

isIdle
isLoading
isIdleOrLoading
isData
isError
lastData: Returns the last successful data value or null. This is useful when you want to display the last valid value while refreshing.

Methods:
All these methods with the exception on reset() and overrideWith() are also available to StreamBeacons.

start(): Starts the future if it's in the idle state.
reset(): Resets the beacon by running the callback again. This will enter the loading state immediately.
unwrapValue(): Returns the value if the beacon is in the data state. This will throw an error if the beacon is not in the data state.
unwrapValueOrNull(): This is like unwrapValue() but it returns null if the beacon is not in the data state.
toFuture(): Returns a future that completes with the value when the beacon is in the data state. This will throw an error if the beacon is not in the data state.
overrideWith(): Replaces the current callback and resets the beacon by running the new callback.

Beacon.stream: #
Creates a StreamBeacon from a given stream.
When a dependency changes, the beacon will unsubscribe from the old stream and subscribe to the new one.
This beacon updates its value based on the stream's emitted values.
The emitted values are wrapped in an AsyncValue, which can be in one of 4 states:idle, loading, data, or error.
If shouldSleep is true(default), it will unsubscribe from the stream if it's no longer being watched.
It will resubscribe once a listener is added or its value is accessed.
This means that it will enter the loading state when woken up.
Can be transformed into a future with mystreamBeacon.toFuture():
var myStream = Stream.periodic(Duration(seconds: 1), (i) => i);

var myBeacon = Beacon.stream(() => myStream);

myBeacon.subscribe((value) {
print(value); // Outputs AsyncLoading(),AsyncData(0),AsyncData(1),AsyncData(2),...
});
copied to clipboard
Beacon.streamRaw: #
Like Beacon.stream, but it doesn't wrap the value in an AsyncValue.
When a dependency changes, the beacon will unsubscribe from the old stream and subscribe to the new one.
If shouldSleep is true(default), it will unsubscribe from the stream if it's no longer being watched.
It will resubscribe once a listener is added or its value is accessed.
One of the following must be true if an initial value isn't provided:

The type is nullable
isLazy is true (beacon must be set before it's read from)

var myStream = Stream.periodic(Duration(seconds: 1), (i) => i);

var myBeacon = Beacon.streamRaw(() => myStream, initialValue: 0);

myBeacon.subscribe((value) {
print(value); // Outputs 0,1,2,3,...
});
copied to clipboard
Beacon.debounced: #
Creates a DebouncedBeacon that will delay updates to its value based on the duration. This is useful when you want to wait until a user has stopped typing before performing an action.
var query = Beacon.debounced('', duration: Duration(seconds: 1));

query.subscribe((value) {
print(value); // Outputs: 'apple' after 1 second
});

// simulate user typing
query.value = 'a';
query.value = 'ap';
query.value = 'app';
query.value = 'appl';
query.value = 'apple';

// after 1 second, the value will be updated to 'apple'
copied to clipboard
Beacon.throttled: #
Creates a ThrottledBeacon that will limit the rate of updates to its value based on the duration.
If dropBlocked is true(default), values will be dropped while the beacon is blocked, otherwise, values will be buffered and emitted one by one when the beacon is unblocked.
const k10ms = Duration(milliseconds: 10);
var beacon = Beacon.throttled(10, duration: k10ms);

beacon.set(20);
expect(beacon.value, equals(20)); // first update allowed

beacon.set(30);
expect(beacon.value, equals(20)); // too fast, update ignored

await Future.delayed(k10ms * 1.1);

beacon.set(30);
expect(beacon.value, equals(30)); // throttle time passed, update allowed
copied to clipboard
Beacon.filtered: #
Creates a FilteredBeacon that will only updates its value if it passes the filter criteria.
The filter function receives the previous and new values as arguments.
The filter function can also be changed using the setFilter method.
Simple Example:
// only positive values are allowed
var pageNum = Beacon.filtered(10, filter: (prev, next) => next > 0);
pageNum.value = 20; // update is allowed
pageNum.value = -5; // update is ignored
copied to clipboard
Example when filter function depends on another beacon:
In this example, posts is a derived future beacon that will fetch the posts whenever pageNum changes.
We want to prevent the user from changing pageNum while posts is loading.
var pageNum = Beacon.filtered(1); // we will set the filter function later

final posts = Beacon.future(() => Repository.getPosts(pageNum.value));

// can't change pageNum while loading
pageNum.setFilter((prev, next) => !posts.isLoading);
copied to clipboard
Extracted from the infinite list example
Beacon.timestamped: #
Creates a TimestampBeacon that attaches a timestamp to each value update.
var myBeacon = Beacon.timestamped(10);
print(myBeacon.value); // Outputs: (value: 10, timestamp: __CURRENT_TIME__)
copied to clipboard
Beacon.undoRedo: #
Creates an UndoRedoBeacon that allows undoing and redoing changes to its value.
var age = Beacon.undoRedo(0, historyLimit: 10);

age.value = 10;
age.value = 20;

age.undo(); // Reverts to 10
age.redo(); // Goes back to 20
copied to clipboard
Beacon.bufferedCount: #
Creates a BufferedCountBeacon that collects and buffers a specified number
of values. Once the count threshold is reached, the beacon's value is updated
with the list of collected values and the buffer is reset.
This beacon is useful in scenarios where you need to aggregate a certain
number of values before processing them together.
var countBeacon = Beacon.bufferedCount<int>(3);

countBeacon.subscribe((values) {
print(values);
});

countBeacon.add(1);
countBeacon.add(2);
countBeacon.add(3); // Triggers update and prints [1, 2, 3]
copied to clipboard
You may also access the currentBuffer as a readable beacon.
See it in use in the konami example;
Beacon.bufferedTime: #
Creates a BufferedTimeBeacon that collects values over a specified time duration.
Once the time duration elapses, the beacon's value is updated with the list of
collected values and the buffer is reset.
var timeBeacon = Beacon.bufferedTime<int>(duration: Duration(seconds: 5));

timeBeacon.subscribe((values) {
print(values);
});

timeBeacon.add(1);
timeBeacon.add(2);
// After 5 seconds, it will output [1, 2]
copied to clipboard
Beacon.list: #
The ListBeacon provides methods to add, remove, and update items in the list and notifies listeners without having to make a copy.
NB: The previousValue and current value will always be the same because the same list is being mutated. If you need access to the previousValue, use Beacon.writable
Beacon.hashSet:
Similar to Beacon.list(), but for Sets.
Beacon.hashMap:
Similar to Beacon.list(), but for Maps.
var nums = Beacon.list<int>([1, 2, 3]);

Beacon.effect(() {
print(nums.value); // Outputs: [1, 2, 3]
});

nums.add(4); // Outputs: [1, 2, 3, 4]

nums.remove(2); // Outputs: [1, 3, 4]
copied to clipboard
AsyncValue: #
An AsyncValue is a wrapper around a value that can be in one of four states:idle, loading, data, or error.
This is the value type of FutureBeacons,FutureBeacons and StreamBeacons.
var myBeacon = Beacon.future(() async {
return await Future.delayed(Duration(seconds: 1), () => 'Hello');
});

print(myBeacon.value); // Outputs AsyncLoading immediately

await Future.delayed(Duration(seconds: 1));

print(myBeacon.value); // Outputs AsyncData('Hello')
copied to clipboard
AsyncValue.unwrap():
Casts this [AsyncValue] to [AsyncData] and return its value. This will throw an error if the value is not an [AsyncData].
var name = AsyncData('Bob');
print(name.unwrap()); // Outputs: Bob

name = AsyncLoading();
print(name.unwrap()); // Throws error
copied to clipboard
AsyncValue.lastData:
Returns the latest valid data value or null. This is useful when you want to display the last valid value while loading new data.
var myBeacon = Beacon.future(() async {
return await Future.delayed(Duration(seconds: 1), () => 'Hello');
});

print(myBeacon.value); // Outputs AsyncLoading immediately

print(myBeacon.value.lastData); // Outputs null as there is no valid data yet

await Future.delayed(Duration(seconds: 1));

print(myBeacon.value.lastData); // Outputs 'Hello'

myBeacon.reset();

print(myBeacon.value); // Outputs AsyncLoading

print(myBeacon.value.lastData); // Outputs 'Hello' as the last valid data when in loading state
copied to clipboard
AsyncValue.tryCatch:
Executes the future provided and returns [AsyncData] with the result
if successful or [AsyncError] if an exception is thrown.
Supply an optional [WritableBeacon] that will be set throughout the
various states.
Supply an optional [optimisticResult] that will be set while loading, instead of [AsyncLoading].
Future<String> fetchUserData() {
// Imagine this is a network request that might throw an error
return Future.delayed(Duration(seconds: 1), () => 'User data');
}
beacon.value = AsyncLoading();
beacon.value = await AsyncValue.tryCatch(fetchUserData);
copied to clipboard
You can also pass the beacon as a parameter.
loading,data and error states,
as well as the last successful data will be set automatically.
await AsyncValue.tryCatch(fetchUserData, beacon: beacon);

// or use the extension method.

await beacon.tryCatch(fetchUserData);
copied to clipboard
See it in use in the shopping cart example.
If you want to do optimistic updates, you can supply an optional optimisticResult parameter.
await beacon.tryCatch(mutateUserData, optimisticResult: 'User data');
copied to clipboard
Without tryCatch, handling the potential error requires more
boilerplate code:
beacon.value = AsyncLoading();
try {
beacon.value = AsyncData(await fetchUserData());
} catch (err,stacktrace) {
beacon.value = AsyncError(err, stacktrace);
}
copied to clipboard
Beacon.family: #
Creates and manages a family of related Beacons based on a single creation function.
This class provides a convenient way to handle related
beacons that share the same creation logic but have different arguments.
Type Parameters: #

T: The type of the value emitted by the beacons in the family.
Arg: The type of the argument used to identify individual beacons within the family.
BeaconType: The type of the beacon in the family.

If cache is true, created beacons are cached. Default is false.
Example:
final postContentFamily = Beacon.family(
(String id) {
return Beacon.future(() async {
return await Repository.getPostContent(id);
});
},
);


final postContent = postContentFamily('post-1');
final postContent = postContentFamily('post-2');

postContent.subscribe((value) {
print(value); // Outputs: post content
});
copied to clipboard
BeaconGroup: #
An alternative to the global beacon creator ie: Beacon.writable(0); that
keeps track of all beacons and effects created so they can be disposed/reset together.
This is useful when you're creating multiple beacons in a stateful widget or controller class
and want to dispose them together. See BeaconController.
final myGroup = BeaconGroup();

final name = myGroup.writable('Bob');
final age = myGroup.writable(20);

myGroup.effect(() {
print(name.value); // Outputs: Bob
});

age.value = 21;
name.value = 'Alice';

myGroup.resetAll(); // reset beacons but does nothing to the effect

print(name.value); // Bob
print(age.value); // 20

myGroup.disposeAll();

print(name.isDisposed); // true
print(age.isDisposed); // true
// All beacons and effects are disposed
copied to clipboard
Properties and Methods: #
myBeacon.value: #
The current value of the beacon. This beacon will be registered as a dependency if accessed within a derived beacon or an effect. Aliases: myBeacon(), myBeacon.call().
myBeacon.peek(): #
Returns the current value of the beacon without registering it as a dependency.
myBeacon.watch(context): #
Returns the current value of the beacon and rebuilds the widgets whenever the beacon is updated.
final name = Beacon.writable("Bob");

class ProfileCard extends StatelessWidget {

@override
Widget build(BuildContext context) {
// rebuilds whenever the name changes
return Text(name.watch(context));
}
}
copied to clipboard
myBeacon.observe(context,callback): #
Executes the callback (side effect) whenever the beacon is updated. eg: Show a snackbar when the value changes.
final name = Beacon.writable("Bob");

class ProfileCard extends StatelessWidget {

@override
Widget build(BuildContext context) {
name.observe(context, (prev, next) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('name changed from $prev to $next')),
);
});

return Text(name.watch(context));
}
}
copied to clipboard
myBeacon.subscribe(): #
Subscribes to the beacon and listens for changes to its value.

final age = Beacon.writable(20);

age.subscribe((value) {
print(value); // Outputs: 21, 22, 23
});

age.value = 21;
age.value = 22;
age.value = 23;
copied to clipboard
myBeacon.toStream(): #
This returns a stream that emits the beacon's value whenever it changes.
myWritable.wrap(anyBeacon): #
Wraps an existing beacon and consumes its values
Supply a (then) function to customize how the emitted values are
processed.
var bufferBeacon = Beacon.bufferedCount<String>(10);
var count = Beacon.writable(5);

// Wrap the bufferBeacon with the readableBeacon and provide a custom transformation.
bufferBeacon.wrap(count, then: (value) {
// Custom transformation: Convert the value to a string and add it to the buffer.
bufferBeacon.add(value.toString());
});

print(bufferBeacon.buffer); // Outputs: ['5']
count.value = 10;
print(bufferBeacon.buffer); // Outputs: ['5', '10']
copied to clipboard
This method is available on all writable beacons, including BufferedBeacons; and can wrap any beacon since all beacons are readable.
myWritable.ingest(anyStream): #
This functions like .wrap() but it's specifically for streams. It listens to the stream and updates the beacon's value with the emitted values.
final beacon = Beacon.writable(0);
final myStream = Stream.fromIterable([1, 2, 3]);

beacon.ingest(myStream);

beacon.subscribe((value) {
print(value); // Outputs: 1, 2, 3
});
copied to clipboard
mybeacon.next(): #
Listens for the next value emitted by this Beacon and returns it as a Future.
This method subscribes to this Beacon and waits for the next value
that matches the optional [filter] function. If [filter] is provided and
returns false for a emitted value, the method continues waiting for the
next value that matches the filter. If no [filter] is provided,
the method completes with the first value received.
If this is a lazy beacon and it's disposed before a value is emitted,
the future will be completed with an error if a [fallback] value is not provided.
final age = Beacon.writable(20);

Timer(Duration(seconds: 1), () => age.value = 21;);

final nextAge = await age.next(); // returns 21 after 1 second
copied to clipboard
mybeacon.toListenable(): #
Returns a ValueListenable that emits the beacon's value whenever it changes.
mybeacon.toValueNotifier(): #
Returns a ValueNotifier that emits the beacon's value whenever it changes. Any mutations to the ValueNotifier will be reflected in the beacon. The ValueNotifier will be disposed when the beacon is disposed.
mybeacon.dispose(): #
Disposes the beacon and releases all resources.
mybeacon.onDispose(): #
Registers a callback to be called when the beacon is disposed. Returrns a function that can be called to unregister the callback.
Chaining methods: #
Seamlessly chain beacons to create sophisticated reactive pipelines, combining multiple functionalities for advanced value manipulation and control.
// every write to this beacon will be filtered then debounced.
final searchQuery = Beacon.writable('').filter((prev, next) => next.length > 2).debounce(duration: k500ms);
copied to clipboard

Important
When chaining beacons, all writes made to the returned beacon will be re-routed to the first writable beacon in the chain. It is recommended to mutate the source beacons directly.

const k500ms = Duration(milliseconds: 500);

final count = Beacon.writable(10);

final filteredCount = count
.debounce(duration: k500ms),
.filter((prev, next) => next > 10);

filteredCount.value = 20;
// The mutation will be re-routed to count
// before being passed to the debounced beacon
// then to the filtered beacon.
// This is equivalent to count.value = 20;

expect(count.value, equals(20));

await Future.delayed(k500ms);

expect(filteredCount.value, equals(20));
copied to clipboard

Warning
buffer and bufferTime cannot be mid-chain. If they are used, they MUST be the last in the chain.

// GOOD
someBeacon.filter().buffer(10);

// BAD
someBeacon.buffer(10).filter();
copied to clipboard
mybeacon.buffer(): #
Returns a Beacon.bufferedCount that wraps this beacon.
NB: The returned beacon will be disposed when the wrapped beacon is disposed.
final age = Beacon.writable(20);

final bufferedAge = age.buffer(10);

bufferedAge.subscribe((value) {
print(value); // Outputs: [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
});

for (var i = 0; i < 10; i++) {
age.value++;
}
copied to clipboard
mybeacon.bufferTime(): #
Returns a Beacon.bufferedTime that wraps this beacon.
mybeacon.throttle(): #
Returns a Beacon.throttled that wraps this beacon.
mybeacon.filter(): #
Returns a Beacon.filtered that wraps this beacon.
mybeacon.map(): #
Returns a [ReadableBeacon] that wraps a Beacon and tranforms its values.
final stream = Stream.periodic(k1ms, (i) => i).take(5);
final beacon = stream
.toRawBeacon(isLazy: true)
.map((v) => v + 1)
.filter((_, n) => n.isEven);

await expectLater(beacon.stream, emitsInOrder([1, 2, 4]));
copied to clipboard

Note
When map returns a different type, writes to the returned beacon will not be re-routed to the original beacon. In the example below, writes to filteredBeacon will NOT be re-routed to count because map returns a String; which means the type of the returned beacon is FilteredBeacon<String> and count holds an int.

final count = Beacon.writable(0);
final filteredBeacon = count.map((v) => '$v').filter((_, n) => n.length > 1);
copied to clipboard
mybeacon.debounce(): #
Returns a Beacon.debounced that wraps this beacon.
final query = Beacon.writable('');

const k500ms = Duration(milliseconds: 500);

final debouncedQuery = query
.filter((prev, next) => next.length > 2)
.debounce(duration: k500ms);
copied to clipboard
Debugging: #
Set the global BeaconObserver instance to get notified of all beacon creation, updates and disposals. You can also see when a derived beacon or effect starts/stops watching a beacon.
You can create your own observer by implementing BeaconObserver or use the provided logging observer, which logs to the console. Provide a name to your beacons to make it easier to identify them in the logs.
BeaconObserver.instance = LoggingObserver(); // or BeaconObserver.useLogging()

var a = Beacon.writable(10, name: 'a');
var b = Beacon.writable(20, name: 'b');
var c = Beacon.derived(() => a() * b(), name: 'c');

Beacon.effect(
() {
print('c: ${c.value}');
},
name: 'printEffect',
);
copied to clipboard
This will log:
Beacon created: a
Beacon created: b
Beacon created: c

"printEffect" is watching "c"

"c" is watching "a"
"c" is watching "b"

"c" was updated:
old: null
new: 200
copied to clipboard
Updating a beacon:
a.value = 15;
copied to clipboard
This will log:
"a" was updated:
old: 10
new: 15

"c" was updated:
old: 200
new: 300
copied to clipboard
Disposing a beacon
c.dispose();
copied to clipboard
This will log:
"c" stopped watching "a"
"c" stopped watching "b"

"c" was disposed
"printEffect" stopped watching c
copied to clipboard
Disposal #
When a beacon is disposed, all downstream derived beacons and effects will be disposed as well.
A beacon cannot be updated after it's disposed. An assertion error will be thrown if you try to update a disposed beacon.
A warning will be logged in debug mode if you try to access the value of a disposed beacon.
A beacon should be disposed when it's no longer needed to free up resources.
In the example below, when a is disposed, c and effect will also be disposed.
a b
\ /
\ /
c
|
|
effect
copied to clipboard
final a = Beacon.writable(10);
final b = Beacon.writable(10);
final c = Beacon.derived(() => a.value * b.value);

Beacon.effect(() => print(c.value));

//...//

a.dispose();

expect(a.isDisposed, true);
expect(c.isDisposed, true);
// effect is also disposed
copied to clipboard
Testing #
Beacons can expose a Stream with the .toStream() method. This can be used to test the state of a beacon over time with existing StreamMatchers.
final count = Beacon.writable(10);

final stream = count.toStream();

Future.delayed(Duration(milliseconds: 1), () => count.value = 20);

expect(stream, emitsInOrder([10, 20]));
copied to clipboard
Testing beacons with chaining methods #
Chaining methods (buffer, bufferTime, next) can be used to make testing easier.
anyBeacon.buffer()
final count = Beacon.writable(10);

final buff = count.buffer(2);

count.value = 20;

expect(buff.value, equals([10, 20]));
copied to clipboard
anyBeacon.next()
final count = Beacon.writable(10);

expectLater(count.next(), completion(30));

count.value = 30;
copied to clipboard
anyBeacon.bufferTime().next()
final count = Beacon.writable(10);

final buffTime = count.bufferTime(duration: Duration(milliseconds: 10));

expectLater(buffTime.next(), completion([10, 20, 30, 40]));

count.value = 20;
count.value = 30;
count.value = 40;
copied to clipboard
BeaconController #
An abstract mixin class that automatically disposes all beacons and effects created within it. This can be used to create a controller that manages a group of beacons. use the included BeaconGroup(B.writable()) instead of Beacon.writable() to create beacons and effects.
NB: All beacons must be created as a late variable.
class CountController extends BeaconController {
late final count = B.writable(0);
late final doubledCount = B.derived(() => count.value * 2);
}
copied to clipboard
Dependency Injection #
Dependency injection refers to the process of providing an instance of a Beacon or BeaconController to your widgets. state_beacon ships with a lightweight dependency injection library called lite_ref that makes it easy and ergonomic to do this while also managing disposal of both.
NB: You can use another DI library such as Provider.
In the example below, the controller will be disposed when the CounterText is unmounted:
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');
}
}
copied to clipboard
final count = countControllerRef.select(context, (c) => c.count);

// is equivalent to
final controller = countControllerRef.of(context);
final count = controller.count.watch(context);
copied to clipboard
You can also use select2 and select3 to watch multiple beacons at once.
final (count, doubledCount) = countControllerRef.select2(context, (c) => (c.count, c.doubledCount));

// is equivalent to
final controller = countControllerRef.of(context);
final count = controller.count.watch(context);
final doubledCount = controller.doubledCount.watch(context);
copied to clipboard
See the full example with testing here.
You can also use Ref.scoped if you wish to provide a top level beacon without putting it in a controller. The beacon will be properly disposed when all widgets that use it are unmounted.
final countRef = Ref.scoped((ctx) => Beacon.writable(0));
final doubledCountRef = Ref.scoped((ctx) => Beacon.derived(() => countRef(ctx).value * 2));

class CounterText extends StatelessWidget {
const CounterText({super.key});

@override
Widget build(BuildContext context) {
final count = countRef.watch(context);
final doubledCount = doubledCountRef.watch(context);
return Text('$count x 2 = $doubledCount');
}
}
copied to clipboard

Note
Even though this is possible, it is recommended to use BeaconControllers whenever possible. In cases where you only need a single beacon, this can be a convenient way to provide it to a widget.

BeaconControllerMixin #
A mixin for StatefulWidget's State class that automatically disposes all beacons and effects created within it.
class MyController extends StatefulWidget {
const MyController({super.key});

@override
State<MyController> createState() => _MyControllerState();
}

class _MyControllerState extends State<MyController>
with BeaconControllerMixin {
// NO need to dispose these manually
late final count = B.writable(0);
late final doubledCount = B.derived(() => count.value * 2);

@override
Widget build(BuildContext context) {
return Container();
}
}
copied to clipboard
TextEditingBeacon #
A beacon that wraps a TextEditingController. All changes to the controller are reflected in the beacon and vice versa.
final beacon = TextEditingBeacon();
final controller = beacon.controller;

controller.text = 'Hello World';
print(beacon.value.text); // Outputs: Hello World
copied to clipboard
Pitfalls #
When using Beacon.future, only beacons accessed before the async gap(await) will be tracked as dependencies.
final counter = Beacon.writable(0);
final doubledCounter = Beacon.derived(() => counter.value * 2);

final futureCounter = Beacon.future(() async {
// This will be tracked as a dependency because it's accessed before the async gap
final count = counter.value;

await Future.delayed(Duration(seconds: count));

// This will NOT be tracked as a dependency because it's accessed after `await`
final doubled = doubledCounter.value;

return '$count x 2 = $doubled';
});
copied to clipboard
When a future depends on multiple future/stream beacons

DON'T:

final futureCounter = Beacon.future(() async {
// in this instance lastNameStreamBeacon will not be tracked as a dependency
// because it's accessed after the async gap
final firstName = await firstNameFutureBeacon.toFuture();
final lastName = await lastNameStreamBeacon.toFuture();

return 'Fullname is $firstName $lastName';
});
copied to clipboard

DO:

final futureCounter = Beacon.future(() async {
// store the futures before the async gap ie: don't use await
final firstNameFuture = firstNameFutureBeacon.toFuture();
final lastNameFuture = lastNameStreamBeacon.toFuture();

// wait for the futures to complete
final (String firstName, String lastName) = await (firstNameFuture, lastNameFuture).wait;

return 'Fullname is $firstName $lastName';
});
copied to clipboard

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Files:

Customer Reviews

There are no reviews.