state_beacon 0.15.0 copy "state_beacon: ^0.15.0" to clipboard
state_beacon: ^0.15.0 copied to clipboard

A reactive primitive and simple state managerment solution for dart and flutter

Overview #

A Beacon is a reactive primitive(signal) and simple state management solution for Dart and Flutter.

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

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

Using an asynchronous function

final counter = Beacon.writable(0);

// The future will be recomputed whenever the counter changes
final derivedFutureCounter = Beacon.derivedFuture(() 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 (derivedFutureCounter.watch(context)) {
      AsyncData<String>(value: final v) => Text(v),
      AsyncError(error: final e) => Text('$e'),
      _ => const CircularProgressIndicator(),
    };
  }
}

Features #

Pitfalls

Beacon.writable: #

Creates a WritableBeacon from a value that can be read and written to.

final counter = Beacon.writable(0);
counter.value = 10;
print(counter.value); // 10

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

Beacon.readable: #

Creates an immutable ReadableBeacon from a value. This is useful for exposing a beacon's value to consumers without allowing them to modify it.

final counter = Beacon.readable(10);
counter.value = 10; // Compilation error


final _internalCounter = Beacon.writable(10);

// Expose the beacon's value without allowing it to be modified
ReadableBeacon<int> get counter => _internalCounter;

Beacon.createEffect: #

An effect is just a function that will re-run whenever one of its dependencies change. An effect runs immediately after creation.

final age = Beacon.writable(15);

Beacon.createEffect(() {
    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!"

Beacon.doBatchUpdate: #

Executes a batched update which allows multiple updates to be batched into a single update. This can be used to optimize performance by reducing the number of update notifications.

final age = Beacon.writable<int>(10);

var callCount = 0;

age.subscribe((_) => callCount++);

Beacon.doBatchUpdate(() {
  age.value = 15;
  age.value = 16;
  age.value = 20;
  age.value = 23;
});

expect(callCount, equals(1)); // There were 4 updates, but only 1 notification

Beacon.derived: #

Creates a DerivedBeacon whose value is derived from a computation function. This beacon will recompute its value everytime one of it's dependencies change.

Example:

final age = Beacon.writable<int>(18);
final canDrink = Beacon.derived(() => age.value >= 21);

print(canDrink.value); // Outputs: false

age.value = 22;

print(canDrink.value); // Outputs: true

Beacon.derivedFuture: #

Creates a DerivedBeacon 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.

If cancelRunning is true (default: true), the results of a current execution will be discarded if another execution is triggered before the current one finishes.

Example:

final counter = Beacon.writable(0);

// The future will be recomputed whenever the counter changes
final derivedFutureCounter = Beacon.derivedFuture(() 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 (derivedFutureCounter.watch(context)) {
    AsyncData<String>(value: final v) => Text(v),
    AsyncError(error: final e) => Text('$e'),
    AsyncLoading() || AsyncIdle() => const CircularProgressIndicator(),
  };
}
}

Can be transformed into a future with myFutureBeacon.toFuture() This can useful when a DerivedFutureBeacon depends on another DerivedFutureBeacon. This functionality is also availabe to regular FutureBeacons and StreamBeacons.

var count = Beacon.writable(0);

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

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

var fullName = Beacon.derivedFuture(() 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';
});

Beacon.debounced: #

Creates a DebouncedBeacon with an initial value and a debounce duration. This beacon delays updates to its value based on the duration.

var myBeacon = Beacon.debounced(10, duration: Duration(seconds: 1));
myBeacon.value = 20; // Update is debounced
print(myBeacon.value); // Outputs: 10
await Future.delayed(Duration(seconds: 1));
print(myBeacon.value); // Outputs: 20

Beacon.throttled: #

Creates a ThrottledBeacon with an initial value and a throttle duration. This beacon limits the rate of updates to its value based on the duration. Updates that occur faster than the throttle duration are ignored.

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

Beacon.filtered: #

Creates a FilteredBeacon with an initial value and a filter function. This beacon updates its value only 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:

var pageNum = Beacon.filtered(10, (prev, next) => next > 0); // only positive values are allowed
pageNum.value = 20; // update is allowed
pageNum.value = -5; // update is ignored

Example when filter function depends on another beacon:

var pageNum = Beacon.filtered(1); // we will set the filter function later

final posts = Beacon.derivedFuture(() async {Repository.getPosts(pageNum.value);});

pageNum.setFilter((prev, next) => posts.value is! AsyncLoading); // can't change pageNum while loading

Beacon.timestamped: #

Creates a TimestampBeacon with an initial value. This beacon attaches a timestamp to each value update.

var myBeacon = Beacon.timestamped(10);
print(myBeacon.value); // Outputs: (value: 10, timestamp: __CURRENT_TIME__)

Beacon.undoRedo: #

Creates an UndoRedoBeacon with an initial value and an optional history limit. This beacon allows undoing and redoing changes to its value.

var undoRedoBeacon = UndoRedoBeacon<int>(0, historyLimit: 10);
undoRedoBeacon.value = 10;
undoRedoBeacon.value = 20;
undoRedoBeacon.undo(); // Reverts to 10
undoRedoBeacon.redo(); // Goes back to 20

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.value = 1;
countBeacon.value = 2;
countBeacon.value = 3; // Triggers update and prints [1, 2, 3]

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.value = 1;
timeBeacon.value = 2;
// After 5 seconds, it will output [1, 2]

Beacon.stream: #

Creates a StreamBeacon from a given stream. 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 three states: loading, data, or error. This can we wrapped in a Throttled or Filtered beacon to control the rate of updates. 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),...
});

Beacon.streamRaw: #

Like Beacon.stream, but it doesn't wrap the value in an AsyncValue. If you don't supply an initial value, the type has to be nullable.

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

Beacon.future: #

Creates a FutureBeacon that initializes its value based on a future. This can be refreshed by calling the reset method.

If manualStart is true, the future will not execute until [start()] is called.

var myBeacon = Beacon.future(() async {
  return await Future.delayed(Duration(seconds: 1), () => 'Hello');
});

myBeacon.subscribe((value) {
  print(value); // Outputs 'Hello' after 1 second
});

Beacon.list: #

Creates a ListBeacon with an initial list value. This beacon manages a list of items, allowing for reactive updates and manipulations of the list.

var nums = Beacon.list<int>([1, 2, 3]);

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

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

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

myWritable.wrap(anyBeacon): #

Wraps an existing beacon and comsumes its values

Supply a (then) function to customize how the emitted values are processed.

var bufferBeacon = Beacon.bufferedCount<int>(10);
var count = Beacon.writable(5);

// Wrap the count beacon and provide a custom transformation.
bufferBeacon.wrap(
  count,
  then: (thisBeacon, countValue) {

      // Custom transformation: Add the value twice to the buffer.
      thisBeacon.add(countValue);
      thisBeacon.add(countValue);

});

print(bufferBeacon.buffer); // Outputs: [5, 5]

count.value = 10;

print(bufferBeacon.buffer); // Outputs: [5, 5, 10, 10]

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 apiClientFamily = Beacon.family(
 (String baseUrl) {
   return Beacon.readable(ApiClient(baseUrl));
 },
);

final githubApiClient = apiClientFamily('https://api.github.com');
final twitterApiClient = apiClientFamily('https://api.twitter.com');

Pitfalls #

When using Beacon.derivedFuture, 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 derivedFutureCounter = Beacon.derivedFuture(() 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';
});

When a derivedFuture depends on multiple future/stream beacons

  • DONT:
final derivedFutureCounter = Beacon.derivedFuture(() 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';
});
  • DO:
final derivedFutureCounter = Beacon.derivedFuture(() async {
  // acquire 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';
});
30
likes
0
pub points
65%
popularity

Publisher

verified publishernosy.dev

A reactive primitive and simple state managerment solution for dart and flutter

Repository (GitHub)
View/report issues

Topics

#state #signal #reactive #beacon

License

unknown (license)

Dependencies

flutter

More

Packages that depend on state_beacon