Query and paginate¶
Filter, sort, and page in the engine, decoding only what you show:
final page = await items
.query()
.where('done', isEqualTo: false)
.orderBy('createdAt', descending: true)
.limit(20)
.page();
// page.items, and:
final next = await items
.query()
.where('done', isEqualTo: false)
.orderBy('createdAt', descending: true)
.limit(20)
.startAfter(page.nextCursor!)
.page();
On the disk engine this runs as SQL. Declare indexedFields: ['done'] when
creating the collection and equality predicates become index searches.
Operators¶
where(path, ...) takes exactly one operator per call; calls combine with
AND:
isEqualTo(a storednullmatchesisEqualTo: null)isGreaterThan/isLessThan(on aPnCounterfield, compares the total)isIn(a non-empty list of scalars)contains(element membership,OrSetfields only)
Every clause is checked against the field's CRDT type when you build the
query; a misdirected clause throws ArgumentError instead of silently
returning nothing.
Stable pages¶
page() returns a cursor, not an offset. The order is
(order-by value, document id): total, identical on every converged device
and on either engine, and stable across a restart. Text compares by Unicode
code point (as SQLite compares UTF-8), not by UTF-16 code unit. Pages are disjoint and gap-free even while the
data changes, which offset-based paging cannot promise.
Live results¶
Watch a query instead of the whole collection:
A watched query re-emits only when its own result changes, and decodes at most
one page per emission. In Flutter, CollectionNotifier.query(...) wraps it.
One constraint: queries need per-field records, so the collection must be
created with a field-granular merge strategy (ShallowMergeStrategy,
DeepMergeStrategy, or SchemaMergeStrategy — for an annotated model, the
generated _$XMergeStrategy). Querying a collection left on the default
ObjectMergeStrategy throws.