Set of extensions on ValueNotifier 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 UI.
Generally, there are two PubSub approaches for UI in Flutter:
ValueNotifiers. Streams are widely used and can be good in some situations and are very powerful, but they come with a price: boilerplate.
Controllers, Streams, Sinks, Sync/Async variations, broadcast/not broadcast streams. That is a lot to type and to keep in mind.
ValueNotifiers, on the other hand, are a lot simpler, lighter, and need less code. But they lack Stream's power – it's hard to make derived notifiers, dispose of them automatically, subscribe and cancel subscriptions and integrated 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((current) => ...) extension provides a better API for setting Notifiers' value.
Consider this example:
/// Traditional someNotifier.value = someNotifier.value * 10; /// Extension someNotifier.set((current) => current * 10);
ValueNotifiers provide 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 = someValueNotifier.subscribe((value) => print(value)); print(subscription.isCanceled); // Prints 'false' someValueNotifier.set((_) => 10); // Prints 10 subscription.cancel() print(subscription.isCanceled); // Prints 'true' someValueNotifier.set((_) => 100); // Does not print
Sometimes, there is a need to convert a
Stream to a
ValueNotifier. This extension does exactly that.
final stream = Stream.periodic(Duration(seconds: 1), (second) => second); final secondsPassed = stream.extractValue(initial: 0);
ValueNotifierss need to be properly disposed in order for resources to be released. Regular approach usually would look like a dispose method with a bunch of
someNotifier.dispose()s in it in the reverse order of their creation.
Value Extensions provide a more pleasant way of doing so –
DisposeBag is a simple container that stores notifiers that need to be disposed. It has a single method –
clear() that disposes every service that was added to it in reverse order.
.disposedBy(disposeBag) is an extension that adds a
ValueNotifier to an instance of a
Example usage would look like this.
final disposeBag = DisposeBag(); final intNotifier = ValueNotifier(0).disposedBy(disposeBag); final stringNotifier = ValueNotifier('Hello, Wold!').disposedBy(disposeBag); // Disposes notifiers in reverse order of being added: // stringNotifier -> intNotifier. disposeBag.clear();
The next section focuses on the transformation of the Notifiers. They implement the Iterator pattern and copy Stream's methods, so when base Notifier changes – derived Notifiers will change too.
Creates a new ValueNotifier, deriving its value from the base one using the transform function.
final stringNotifier = ValueNotifier('Hello'); final lengthNotifier = stringNotifier.map((value) => value.length); // 5 stringNotifier.set((current) => current + ', World!'); // lengthNotifier value becomes 13
Works as a regular
.map(), but takes a function that returns a ValueNotifier and "flattens" the result.
So instead of ValueNotifier<ValueNotifier<T>>, it becomes a regular ValueNotifier<T>.
final intNotifier = ValueNotifier(0); final stringNotifier = ValueNotifier('Hello!'); final isShowingInt = ValueNotifier(true); final currentNotifier = isShowingInt.flatMap((isInt) => isInt ? intNotifier : stringNotifier);
Creates a new Notifier by filtering base Notifier's values, not including the first one.
final intNotifier = ValueNotifier(1); final evenNotifier = intNotifier.where((value) => value.isEven); // 1 final oddNotifier = intNotifier.where((value) => value.isOdd); // 1 // evenNotifier: 2 // oddNotifier: 1 intNotifier.set((value) => value + 1);
Creates a new Notifier by combining its value with
other's value and feeding the pair to the transform function.
final stringNotifier = ValueNotifier('Flutter'); final boolNotifier = ValueNotifier(false); final textNotifier = stringNotifier.combineLatest<bool>( boolNotifier, (stringValue, boolValue) => boolValue ? stringValue.toUpperCase() : stringValue, ); // Flutter boolNotifier.set((_) => true); // textNotifier value becomes FLUTTER
Wrapper around the
.combineLatest(...) extension. Packs latest values of the Notifiers in a tuple. Useful in UI to avoid nestings.
final intNotifier = ValueNotifier(0); final boolNotifier = ValueNotifier(false); final paralleledNotifier = intNotifier.parallelWith(boolNotifier); // (0, false) intNotifier.set((current) => current + 1); // paralleledNotifier becomes (1, false) boolNotifier.set((_) => true); // paralleledNotifier becomes (1, true)
The last section focuses on UI integrations.
Both Streams and ValueNotifiers are intended to be displayed in the UI through Builders. The only problem is – builders are massive. To "bind" a single observable variable to the UI, one must type around 86 characters. Value Extensions reduces this number to around 23 characters or almost 4 times less!
To bind a ValueNotifier to the UI, the
.bind(...) extension is used.
Consider these two examples: using
ValueListenableBuilder( valueListenable: nameNotifier, builder: (context, name, child) => Text("Hello, $name!"), )
nameNotifier.bind( (name) => Text("Hello, $name!"), )
.bind(...) extension uses
ValueListenableBuilder inside, you have the same efficient targeted rebuilds, but with less code to type.
Sometimes, it is convenient to create a new
ValueNotifier in the UI, combining notifiers from different state objects. And to be sure that resources are being released properly, the
DisposableBuilder can be used.
The most frequent use of the
DisposableBuilder is in combination with the
parallelWith(...) extension. Example usage would look like this.
DisposableBuilder( builder: (context, disposeBag) => state.stringCounterValue .parallelWith(state.counterColor) .disposedBy(disposeBag) .bind( (text, color) => Text( text, style: TextStyle(color: color), ), ), ),
Note – the
.bind(...) extension used on the Notifier obtained from the
.parallelWith(...) extension automatically destructures its value in the Widget callback.