flutterx_live_data 2.0.1
flutterx_live_data: ^2.0.1 copied to clipboard
Observable data holders inspired by Android LiveData, with lazy activation, mediators, history, storage, observable collections and widget builders.
flutterx_live_data #
Observable data holders inspired by Android LiveData: lazy mediators, history, persistence on a
Map<String, String>, observable collections and widget builders.
Installation #
flutter pub add flutterx_live_data
import 'package:flutterx_live_data/flutterx_live_data.dart';
Concepts #
| Need | Type |
|---|---|
| A value that changes over time | MutableLiveData<T> (read-only view: LiveData<T>) |
| A constant that fits where an observable is expected | ImmutableLiveData<T> |
| One-shot events (navigation, snackbars) | LiveEvent<T> |
| A value derived from other data | MediatorLiveData<T>, data.transform(...) |
| A list, map, set or queue that notifies | LiveList, LiveMap, LiveSet, LiveQueue |
| Undo/redo | HistoryMutableLiveData and the History* collections |
| A value that survives restarts | StorageMutableLiveData and the Storage* collections |
| An object that notifies when its fields change | SelfLiveData |
A data without a value is not initialized: initialized is false, valueOrNull is null and value throws
StateError. MutableLiveData<int>(value: 0) starts initialized; MutableLiveData<int>.late() does not. A new
observer receives the current value at once, then every change; a value equal to the current one (equality, == by
default) is not notified.
Widgets #
final counter = MutableLiveData<int>(value: 0);
LiveDataBuilder<int>(
data: counter,
placeholder: const CircularProgressIndicator(),
builder: (context, value) => Text('$value'),
)
MultipleLiveDataBuilder.with2<int, String>(
x1: counter,
x2: name,
builder: (context, count, name) => Text('$name: $count'),
)
with2 to with7 are typed; MultipleLiveDataBuilder(data: [...], builder: (context, values) => ...) takes a list.
Both rebuild once at the end of the next frame, however many changes happened. In a State, declare observers once
and let the mixin align them:
class _PageState extends State<Page> with ObserverMixin, StateObserver<Page> {
@override
void registerObservers() {
events.observe(this, (message) => showMessage(message));
observe2(user, settings, (user, settings) => sync(user, settings), dispatcher: Dispatcher.postFrame);
}
}
registerObservers runs again on every dependency or widget change: observers still declared are kept, new ones
added, dropped ones disposed, and all on dispose. observe* outside registerObservers throws StateError.
Outside widgets use observeForever, which returns a Disposable:
final subscription = counter.observeForever(print);
subscription.dispose();
ObservableData is a Listenable and LiveData a ValueListenable, so ListenableBuilder,
ValueListenableBuilder and Listenable.merge work.
Mediators and transformations #
final label = counter.transform((value) => value.isEven ? 'even' : 'odd');
final total = MediatorLiveData<int>()..addTransformSource2<int, int>(a, b, (a, b) => a + b);
final user = MediatorLiveData<User>()
..initialize(() => repository.load(), invalidate: refreshTrigger);
Transforms may return a Future; the last one started wins. A mediator follows its sources only while observed;
reactive: true keeps it following them always. Use addSource for a custom callback and invalidateSource /
invalidateSources to run callbacks again. MediatorMutableLiveData, MediatorLiveEvent and the Mediator*
collections add the same behavior to the other types.
Events #
final messages = LiveEvent<String>();
messages.send('saved');
messages.postSend(repository.save().then((_) => 'saved'));
An observer gets only events sent after it registered. Equal consecutive events are all delivered, and with a
Dispatcher every event arrives, in order; pending futures hold back the events posted after them.
Collections #
final todos = LiveList<String>();
todos.add('milk');
LiveDataBuilder<List<String>>(data: todos, builder: (context, list) => Text('${list.length}'))
Collections implement List, Map, Set and Queue, and notify with themselves only when a mutation changes
something. apply((data) => ...) mutates the underlying collection and notifies when the callback returns true;
setTo replaces the content. LiveSelectableData holds the item of a list picked by id.
History #
final text = HistoryMutableLiveData<String>(value: '');
text.value = 'a';
text.historyUndo();
LiveDataBuilder<bool>(data: text.canUndo, builder: (context, can) => ...)
Values are recorded as strings (JSON by default; pass encode/decode for other types), consecutive equal ones once,
up to capacity (20). Also historyRedo, historyReplace, historyClear, index.
Persistent data #
Storage* data persist in any Map<String, String>, such as localStorage of
flutterx_preferences:
final theme = StorageMutableLiveData<ThemeMode>(
storage: StorageInterface.enumValue(
storage: localStorage,
key: 'theme',
values: ThemeMode.values,
defaultValue: ThemeMode.system,
),
);
StorageInterface has factories for String, int, double, bool, enums, JSON objects, lists and maps. A value
equal to the default is not stored but removed; an unreadable stored value falls back to the default, reported with
FlutterError.reportError. reset() restores the default. The same StorageInterface feeds StorageLiveList,
StorageLiveMap, StorageLiveSet and StorageLiveQueue; without a stored value or a default the data is
uninitialized and the collections start empty.
Dispatchers #
observeForever and the observe* methods call synchronously unless given a Dispatcher: Dispatcher.microtask,
Dispatcher.postFrame (after a frame, never synchronous) or Dispatcher.endOfFrame. Changes arriving before the call
runs are coalesced into the latest value, except for events.
Behavior to know #
- Values: the last write wins.
postValuewith a pendingFutureis superseded by any latervalue =orpostValue; a failed future is reported withFlutterError.reportErrorand leaves the value. - A closure registered twice with
observeForever, or a source added twice withaddSource, on the same data throwsStateError;Disposable.disposeis idempotent.removeObserverasserts the observer exists. addListenerlisteners are synchronous, get no value, and count as observers: they activate lazy mediators.ValueListenableBuilderreadsvaluebefore listening: a mediator that is notreactivehas no value yet and throws. UseLiveDataBuilderorreactive: true.ImmutableLiveDatakeeps no registration: observers get the value once, and the same closure may be registered again.- Exceptions thrown by observers are reported with
FlutterError.reportError, not propagated to the writer.
Claude Code plugin #
The repository ships a plugin with skills that teach Claude Code to integrate the library:
/plugin marketplace add https://gitlab.com/dade.sliep/flutterx_live_data
/plugin install flutterx-live-data@flutterx-live-data