Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/mainser/cindel/llms.txt

Use this file to discover all available pages before exploring further.

Cindel exposes every watched scope as a standard Dart Stream, which means you can use it directly with Flutter’s StreamBuilder, call .listen() from any class, or compose it with other stream operators. Watchers are backed by the database’s change-notification layer and only fire after a write transaction commits successfully — a rolled-back transaction never notifies any subscriber.

Watching a Single Object

watchObject(id) emits a typed snapshot of the object every time its underlying document changes. When the object is deleted the stream emits null:
final sub = db.users.watchObject(ada.dbId).listen((user) {
  if (user == null) {
    print('User was deleted.');
  } else {
    print('User updated: ${user.name}');
  }
});
By default watchObject fires immediately with the current stored value (fireImmediately: true). Pass fireImmediately: false to receive only future changes:
final sub = db.users.watchObject(
  ada.dbId,
  fireImmediately: false,
).listen((user) {
  // Called only when the document actually changes.
});

Watching an Entire Collection

watchCollection() emits the full typed list of every object in the collection on each change. The snapshot includes all documents in their natural order:
final sub = db.users.watchCollection().listen((users) {
  print('Collection has ${users.length} users.');
});
Like watchObject, the default is fireImmediately: true. Supply fireImmediately: false to skip the initial emission.

Watching a Query

Attach .watch() to any generated query chain. The stream emits the typed list of matching objects after each commit that could affect the result:
final sub = db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .watch()
    .listen((activeUsers) {
      print('${activeUsers.length} active users');
    });
Query watchers fire when any write to the collection completes, not only writes that provably change the result set. Filtering down to only changed results happens inside Cindel using the distinct-snapshot logic baked into the typed collection layer.

Lazy Watchers

Lazy watchers emit a void invalidation signal rather than materializing the objects. They are useful when you maintain your own local cache and only need to know that something changed, not what changed:
final sub = db.users.watchCollectionLazy().listen((_) {
  // Re-read or re-query the data yourself.
  _refreshCache();
});
watchObjectLazy(id) provides the same behaviour at the single-object level:
final sub = db.users.watchObjectLazy(ada.dbId).listen((_) {
  // Invalidate a cached view of this user.
});
Both lazy variants default to fireImmediately: false so they do not emit on subscription — only future changes trigger the callback.
Prefer lazy watchers for large collections or when the watched scope changes frequently. Because lazy watchers skip object deserialization, they avoid materializing the entire collection on every commit that touches it.

Cancelling Subscriptions

All watcher methods return a standard StreamSubscription. Cancel it when the subscribing widget or scope disposes to avoid memory leaks:
StreamSubscription<List<User>>? _sub;

@override
void initState() {
  super.initState();
  _sub = db.users.watchCollection().listen((users) {
    setState(() => _users = users);
  });
}

@override
void dispose() {
  _sub?.cancel();
  super.dispose();
}

Flutter Widget Example

The most common Flutter pattern is StreamBuilder, which handles subscription lifetime automatically:
class ActiveUsersPage extends StatelessWidget {
  const ActiveUsersPage({super.key, required this.db});

  final CindelDatabase db;

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<User>>(
      stream: db.users
          .filter()
          .activeEqualTo(true)
          .sortByName()
          .watch(),
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return const CircularProgressIndicator();
        }
        final users = snapshot.requireData;
        return ListView.builder(
          itemCount: users.length,
          itemBuilder: (context, index) => ListTile(
            title: Text(users[index].name),
          ),
        );
      },
    );
  }
}
The watch() stream starts with fireImmediately: true by default, so snapshot.hasData becomes true after the first emission before any user interaction occurs.

Build docs developers (and LLMs) love