value_extensions

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.

Motivation

Generally, there are two PubSub approaches for UI in Flutter: Streams and 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.

Getting started

Down are listed all extensions with the use cases. For further information, visit API Reference or the Example.

Core extensions

Those extensions provide base functionality: creating, setting, subscribing, and disposing of Notifiers.

Set

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

Subscribe

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

Extract value

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

Dispose

Just as Streams, 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 and .disposedBy(disposeBag).

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 DisposeBag.

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

Transformers

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.

Map

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

Flat map

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

Where

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

Combine latest

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

Parallel with

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)

UI integrations

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!

Bind

To bind a ValueNotifier to the UI, 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, you have the same efficient targeted rebuilds, but with less code to type.

Disposable builder

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.

Libraries

value_extensions