crdt_lf_flutter 0.2.0 copy "crdt_lf_flutter: ^0.2.0" to clipboard
crdt_lf_flutter: ^0.2.0 copied to clipboard

Flutter reactivity for crdt_lf — rebuild widgets when CRDT state changes, with selectors, a provider and a collaborative text field. Wraps provider, like flutter_bloc.

CRDT LF Flutter #

package badge pub points pub likes codecov ci badge license pub publisher

Flutter reactivity for crdt_lf: rebuild your widgets when CRDT state changes — at the document level or scoped to a single handler, with selectors and a collaborative text field.

Built on top of provider — so you also get context.read / context.watch / context.select for a CRDTDocument for free (provider is re-exported minimally).

Features #

  • CrdtProvider — provide a CRDTDocument to the subtree, created and owned by the provider (CrdtProvider(create:)) or caller-owned (CrdtProvider.value(value:)). Wraps provider's InheritedProvider.
  • CrdtBuilder / CrdtSelector<R> — rebuild on every document update, or only when a selected slice changes.
  • CrdtHandlerBuilder<H> / CrdtHandlerSelector<H, R> — rebuild only when a specific handler changes (optionally including nested handlers).
  • CrdtHandlerListener<H> — side-effect callback on a handler change.
  • CrdtTextFieldBuilder — a TextEditingController bound to a text handler, the way collaborative editor bindings work.
  • CrdtTextCursorsOverlay — paints collaborators' carets/selections over the text field, anchored by stable positions.
  • CrdtAwarenessCursorsOverlay — overlays collaborators' mouse-style presence cursors (pointer arrow + name bubble) on any pane. Split into a CrdtAwarenessCursorsBuilder (positioning + local-pointer handling, you build each marker) and a standalone CrdtAwarenessCursorMarker widget, styled through CrdtAwarenessCursorStyle.
  • Context helpers: context.crdtDocument, context.watchCrdtDocument(), context.selectCrdtDocument(...), context.crdtHandler<H>(id).

Example #

The example/ app demos every widget in this package on a single screen — each card wires a different handler, and a live ⟳ rebuilt ×N badge shows exactly which regions re-render as you edit, so you can see the reactive scoping (and the zero-rebuild bindings) at work.

crdt_lf_flutter_example

Getting Started #

dependencies:
  crdt_lf_flutter: 
  crdt_lf: 

Usage #

Provide a document #

// Owning mode: the provider creates the document lazily and disposes it.
CrdtProvider(
  create: (_) => CRDTDocument(),
  child: const MyApp(),
);

// Value mode: you own the document lifecycle.
CrdtProvider.value(value: doc, child: const MyApp());

Document-level rebuilds #

// Rebuild on any change.
CrdtBuilder(
  builder: (context, document) => Text(textHandler.value),
);

// Rebuild only when a derived slice changes.
CrdtSelector<int>(
  selector: (context, document) => listHandler.value.length,
  builder: (context, count) => Text('$count todos'),
);

Handler-scoped rebuilds #

Rebuild only when one handler changes — unrelated handlers don't trigger a rebuild. CrdtHandlerBuilder hands you the concrete typed handler (the base Handler exposes no value), which is also the right tool for list/map handlers whose value is mutated in place (a value Selector wouldn't detect the change).

CrdtHandlerBuilder<CRDTListHandler<String>>(
  id: 'todos',
  builder: (context, handler) => Text('${handler.value.length}'),
);

// Derived slice from a handler.
CrdtHandlerSelector<CRDTListHandler<String>, int>(
  id: 'todos',
  selector: (context, handler) => handler.value.length,
  builder: (context, count) => Text('$count'),
);

// Also rebuild when a nested/descendant handler changes.
CrdtHandlerBuilder<CRDTMapRefHandler>(
  id: 'root',
  nested: true,
  builder: (context, handler) => ...,
);

// Side effects only (no rebuild).
CrdtHandlerListener<CRDTListHandler<String>>(
  id: 'todos',
  listener: (context, handler) => showSnackBar(...),
  child: ...,
);

Imperative access #

For actions (insert/delete/change) you don't need reactivity — read the handler once:

context.crdtHandler<CRDTListHandler<String>>('todos').insert(0, 'new');

Collaborative text #

CrdtTextFieldBuilder binds a TextEditingController to the text handler (CRDTTextHandler or CRDTFugueTextHandler) registered under id, the way collaborative editor bindings do:

  • local edits are pushed into the handler immediately as the precise delta of each editing gesture (prefix/suffix trimming, one transaction per gesture — no full-text diff, no debounce);
  • IME composition (CJK input, autocorrect) is respected: nothing is committed while a composing region is active;
  • remote changes are adopted into the controller in place, with the caret and selection kept anchored: with a CRDTFugueTextHandler through stable positions (stablePositionAt — anchors tied to element identity, exact even for multi-region remote changes), otherwise mapped through the remote delta;
  • the subtree never rebuildsbuilder runs once and the controller is updated directly.
CrdtTextFieldBuilder(
  id: 'note',
  builder: (context, controller) => TextField(controller: controller),
);

The delta primitives are exported too (TextDelta, computeTextDelta, mapOffsetThroughDelta) if you need to build a custom binding.

Remote text cursors

Publish the local selection with onSelectionAnchorsChanged (the anchors are serializable — send them over your presence channel, e.g. the awareness plugin of crdt_socket_sync) and draw collaborators with CrdtTextCursorsOverlay. Anchors are reported only while the field has focus — on blur the callback fires once with nulls, so with several bound fields a peer shows at most one cursor, where they are typing:

CrdtTextFieldBuilder(
  id: 'note',
  onSelectionAnchorsChanged: (base, extent) => publishPresence(base, extent),
  builder: (context, controller) => CrdtTextCursorsOverlay(
    id: 'note',
    cursors: remoteTextCursors, // List<CrdtTextCursor> from presence
    child: TextField(controller: controller),
  ),
);

Presence cursors #

CrdtAwarenessCursorsOverlay draws collaborators' mouse pointers (arrow plus name bubble) over any pane — the presence complement of the in-field text cursors above. It is transport-agnostic: you map your presence channel to CrdtAwarenessCursors (positions normalized into [0, 1], so cursors map across window sizes) and publish what onLocalPointer reports:

CrdtAwarenessCursorsOverlay(
  cursors: remotePointers, // List<CrdtAwarenessCursor> from presence
  onLocalPointer: (position, {required hovering}) =>
      publishPresence(position, hovering),
  child: pane,
);

The overlay is composed of three pieces, all exported so you can use exactly the layer you need:

  • CrdtAwarenessCursorsBuilder — the transport/layout half: it positions each pointer over the pane and reports the local pointer, but delegates each cursor's look to a builder callback. Use it to draw a completely custom marker (an avatar, a badge, …) per cursor.
  • CrdtAwarenessCursorMarker — the standalone default marker (arrow + name bubble) for one cursor, positioning-agnostic.
  • CrdtAwarenessCursorsOverlay — the ready-made combination of the two above.

The marker is styled Container-style: pass a plain color for the common case, or a full CrdtAwarenessCursorStyle (the "decoration") — its color plus the label text style and marker sizes — when you need more (passing both is an error). Pass a style to CrdtAwarenessCursorsOverlay to restyle every cursor at once, or set CrdtAwarenessCursor.style for a single peer; in the overlay the peer's own color always wins over the style's, so peers keep their identity color:

CrdtAwarenessCursorsOverlay(
  cursors: remotePointers,
  style: const CrdtAwarenessCursorStyle(
    pointerSize: 22,
    labelStyle: TextStyle(fontSize: 12, color: Colors.white),
  ),
  child: pane,
);

(deep dive) How it works #

crdt_lf exposes a CRDTDocument.updates broadcast stream that fires on any local edit, applied remote change or snapshot import. CrdtProvider exposes the document through provider and bridges updates so provider dependents (context.watch / context.select, and the widgets above) rebuild automatically.

Handler-scoped widgets rebuild only when a per-handler signal changes: the O(1) CRDTDocument.revisionForHandler(id), a monotonic revision that grows on every applied change targeting the handler (local or imported) and on snapshot imports carrying its state. With nested: true the ids and revisions of the handler and its descendants (ContainerHandler.childRefs) are folded into one hash, so structural changes (a child added or removed) are detected too.

Packages #

Other bricks of the crdt "system" are:

0
likes
0
points
141
downloads

Publisher

verified publishermattiapispisa.it

Weekly Downloads

Flutter reactivity for crdt_lf — rebuild widgets when CRDT state changes, with selectors, a provider and a collaborative text field. Wraps provider, like flutter_bloc.

Homepage
Repository (GitHub)
View/report issues

Topics

#crdt #local-first #flutter #reactive

License

unknown (license)

Dependencies

crdt_lf, flutter, provider

More

Packages that depend on crdt_lf_flutter