crdt_lf_flutter 0.2.0
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 #
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 aCRDTDocumentto the subtree, created and owned by the provider (CrdtProvider(create:)) or caller-owned (CrdtProvider.value(value:)). Wrapsprovider'sInheritedProvider.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— aTextEditingControllerbound 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 aCrdtAwarenessCursorsBuilder(positioning + local-pointer handling, you build each marker) and a standaloneCrdtAwarenessCursorMarkerwidget, styled throughCrdtAwarenessCursorStyle.- 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.
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
CRDTFugueTextHandlerthrough stable positions (stablePositionAt— anchors tied to element identity, exact even for multi-region remote changes), otherwise mapped through the remote delta; - the subtree never rebuilds —
builderruns 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 abuildercallback. 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: