flutterx_live_data 2.0.1 copy "flutterx_live_data: ^2.0.1" to clipboard
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. postValue with a pending Future is superseded by any later value = or postValue; a failed future is reported with FlutterError.reportError and leaves the value.
  • A closure registered twice with observeForever, or a source added twice with addSource, on the same data throws StateError; Disposable.dispose is idempotent. removeObserver asserts the observer exists.
  • addListener listeners are synchronous, get no value, and count as observers: they activate lazy mediators.
  • ValueListenableBuilder reads value before listening: a mediator that is not reactive has no value yet and throws. Use LiveDataBuilder or reactive: true.
  • ImmutableLiveData keeps 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
14
likes
150
points
422
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Observable data holders inspired by Android LiveData, with lazy activation, mediators, history, storage, observable collections and widget builders.

Repository (GitLab)
View/report issues

License

BSD-3-Clause (license)

Dependencies

collection, flutter

More

Packages that depend on flutterx_live_data