Set of extensions on ValueListenable that allows to use it as Streams in the Rx way.
This package contains transformers, convenience objects & methods, and Widgets to integrate all listed entities into your Widgets.
Generally, there are two PubSub approaches for Widgets in Flutter: Stream
s and Listenable
s. Streams are widely used and can be good in some situations and are very powerful, but they come with a price: boilerplate.
To have a stateful Stream
with a getter of its last value it is needed to:
- Create a
StreamController
- Expose a
Stream
- Create a mutable cache variable
- Expose a getter of that variable
- Every time the cached value is mutated, the new value must be added in the
Sink
And to use it in Widgets through StreamBuilder
, it also needed to provide both the stream
argument and the initialValue
argument.
That's a lot of code.
ValueListenable
s, on the other hand, are a lot simpler and need a lot less code. But they lack Stream's power – it's hard to make derived notifiers, dispose of them automatically, subscribe and cancel subscriptions, and integrate them without crazy nesting in UI. Value Extensions solves this problem.
Down are listed all extensions with the use cases. For further information, visit API Reference or the Example.
Those extensions provide base functionality: creating, setting, subscribing, and disposing of Notifiers.
.set(...)
extension provides a better API for setting ValueNotifier
's value to another and is strictly equivalent to the default .value
setter. Using the .set
method is more concise and states the intent a bit better. The following two examples are equivalent:
someNotifier.value = 10;
someNotifier.set(10);
.update((current) => ...)
extension provides a better API for setting ValueNotifier
's value when the new value depends on the previous one.
Consider this example:
int timesTwo(int x) => x * 2;
/// Traditional
someNotifier.value = timesTwo(someNotifier.value);
/// Extension
someNotifier.update(timesTwo);
ValueListenable provides subscription functionality out of the box, but it is a bit tricky to get information about their status and cancel them. This extension provides functionality for these cases.
final subscription = someValueListenable.subscribe(print);
print(subscription.isCanceled); // Prints 'false'
someValueListenable.set((_) => 10); // Prints 10
subscription.cancel()
print(subscription.isCanceled); // Prints 'true'
someValueListenable.set((_) => 100); // Does not print
It is also possible to pause and resume a subscription.
Sometimes, there is a need to convert a Stream
to a ValueListenable
. This extension does exactly that.
final stream = Stream.periodic(Duration(seconds: 1), (second) => second);
final secondsPassed = stream.extractValue(initial: 0);
The next section focuses on the transformation of the ValueListenables. They implement the Iterator pattern and copy Stream's methods, so when base ValueListenable changes – derived ValueListenables will change too.
Creates a new ValueListenable, deriving its value from the base one using the transform function.
final stringNotifier = ValueNotifier('Hello');
final lengthListenable = stringNotifier.map((value) => value.length); // 5
stringNotifier.update((hello) => '$hello, World!'); // lengthNotifier value becomes 13
Works like map
but transform function must return another ValueListenable
instead of regular value, recomputing its value whenever the ValueListenable
on which the flatMap
was called changes.
final intNotifier = ValueNotifier(0);
final stringNotifier = ValueNotifier('Hello!');
final lengthListenable = stringNotifier.map((string) => string.length);
final isShowingInt = ValueNotifier(true);
final currentValue = isShowingInt.flatMap((isInt) => isInt ? intNotifier : lengthListenable);
Creates a new ValueListenable
by filtering base ValueListenable
's values, not including the first one.
final intNotifier = ValueNotifier(1);
final evenListenable = intNotifier.where((value) => value.isEven); // 1
final oddListenable = intNotifier.where((value) => value.isOdd); // 1
// evenListenable: 2
// oddListenable: 1
intNotifier.update((value) => value + 1);
Creates a new ValueListenable
by combining its value with other
's value and feeding the pair to the transform function.
final word = ValueNotifier('Flutter');
final isUppercased = ValueNotifier(false);
final textListenable = word.combineLatest<bool, String>(
isUppercased,
(word, isUppercased) => isUppercased ? word.toUpperCase() : word,
); // Flutter
isUppercased.set(true); // textNotifier value becomes FLUTTER
Wrapper around the .combineLatest(...)
extension. Packs latest values of the ValueListenable
in a tuple. Useful in Widgets to avoid nestings of ValueListenableBuilder
s or .bind
calls.
final intNotifier = ValueNotifier(0);
final boolNotifier = ValueNotifier(false);
final paralleled = intNotifier.parallelWith(boolNotifier); // (0, false)
intNotifier.update((current) => current + 1); // paralleled becomes (1, false)
boolNotifier.set(true); // paralleled becomes (1, true)
The last section focuses on Widgets integrations.
Both Streams
and ValueListenable
s are intended to be displayed in the UI through Builders. The only problem is – builders are pretty verbose. To "bind" a single observable variable to some Widget, one must type around 86 characters. Value Extensions reduces this number to around 23 characters or almost 4 times less!
To bind a ValueListenable
to a Widget, the .bind(...)
extension is used.
Consider these two examples: using ValueListenableBuilder
and .bind(...)
.
ValueListenableBuilder(
valueListenable: nameNotifier,
builder: (context, name, child) => Text("Hello, $name!"),
)
nameNotifier.bind(
(name) => Text("Hello, $name!"),
)
The .bind(...)
extension uses ValueListenableBuilder
inside, so it results in the same efficient targeted rebuilds, but with less code to type.
Note – the .bind(...)
extension used on the ValueListenable
obtained from the .parallelWith(...)
extension automatically destructures its value in the builder callback.