Watchers
React to database changes in real-time
Watchers
Watchers allow you to subscribe to changes in your database and react efficiently. Perfect for real-time UI updates and sync operations.
Watchers notify you after a transaction commits successfully and the target actually changes.
Overview
You can watch:
- Specific objects - Get notified when one object changes
- Collections - Get notified when any object in a collection changes
- Queries - Get notified when query results change
Watching Objects
Watch a specific object by its ID:
@collection
class User {
Id? id;
late String name;
late int age;
}// Get the updated object
Stream<User?> userStream = isar.users.watchObject(5);
userStream.listen((user) {
if (user == null) {
print('User deleted');
} else {
print('User changed: ${user.name}');
}
});
// Trigger changes
final user = User()..id = 5..name = 'David'..age = 25;
await isar.writeAsync((isar) => isar.users.put(user));
// Output: User changed: David
user.name = 'Mark';
await isar.writeAsync((isar) => isar.users.put(user));
// Output: User changed: Mark
await isar.writeAsync((isar) => isar.users.delete(5));
// Output: User deleted// Just get notified, don't fetch object
Stream<void> userStream = isar.users.watchObjectLazy(5);
userStream.listen((_) {
print('User 5 changed');
});
final user = User()..id = 5..name = 'David'..age = 25;
await isar.writeAsync((isar) => isar.users.put(user));
// Output: User 5 changedThe object doesn't need to exist yet. The watcher will notify you when it's created.
Fire Immediately
Get the current value immediately:
Stream<User?> userStream = isar.users.watchObject(
5,
fireImmediately: true,
);
userStream.listen((user) {
print('User: ${user?.name}');
});
// Immediately outputs current value (or null)Watching Collections
Watch all changes in a collection:
// Just get notified
Stream<void> usersStream = isar.users.watchLazy();
usersStream.listen((_) {
print('A user changed');
});
await isar.writeAsync((isar) async {
isar.users.put(User()..name = 'Alice');
});
// Output: A user changed// Get all objects on each change
Stream<List<User>> usersStream = isar.users.watch();
usersStream.listen((users) {
print('Users: ${users.map((u) => u.name).join(', ')}');
});
await isar.writeAsync((isar) async {
isar.users.put(User()..name = 'Alice');
});
// Output: Users: AliceWatching collections with full objects can be expensive for large collections!
Watching Queries
Watch specific query results:
@collection
class User {
Id? id;
late String name;
late int age;
}// Build a query
final adultsQuery = isar.users
.where()
.ageGreaterThan(18)
.build();
// Watch query results
Stream<List<User>> adultsStream = adultsQuery.watch(
fireImmediately: true,
);
adultsStream.listen((adults) {
print('Adults: ${adults.map((u) => u.name).join(', ')}');
});
// Immediately outputs current results
// Add a child (no notification)
await isar.writeAsync((isar) async {
isar.users.put(User()..name = 'Child'..age = 10);
});
// No output - doesn't match query
// Add an adult (triggers notification)
await isar.writeAsync((isar) async {
isar.users.put(User()..name = 'Alice'..age = 25);
});
// Output: Adults: Alice
// Add another adult
await isar.writeAsync((isar) async {
isar.users.put(User()..name = 'Bob'..age = 30);
});
// Output: Adults: Alice, BobQuery watchers only notify when results actually change!
Lazy Query Watching
final adultsQuery = isar.users
.where()
.ageGreaterThan(18)
.build();
Stream<void> adultsStream = adultsQuery.watchLazy();
adultsStream.listen((_) {
print('Adult users changed');
});Query Watcher Limitations
When using offset, limit, or distinct, watchers may notify even when visible results haven't changed.
// May over-notify
final topUsers = isar.users
.where()
.sortByAge()
.limit(10)
.build();
topUsers.watch().listen((users) {
// Might trigger even if top 10 didn't change
});Real-World Examples
Flutter UI Updates
class UserListWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<List<User>>(
stream: isar.users.where().watch(fireImmediately: true),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text('Age: ${user.age}'),
);
},
);
},
);
}
}User Profile
class UserProfileWidget extends StatelessWidget {
final int userId;
const UserProfileWidget({required this.userId});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: isar.users.watchObject(userId, fireImmediately: true),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Text('User not found');
}
final user = snapshot.data!;
return Column(
children: [
Text('Name: ${user.name}'),
Text('Age: ${user.age}'),
],
);
},
);
}
}Search Results
class SearchWidget extends StatefulWidget {
@override
_SearchWidgetState createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
String searchQuery = '';
@override
Widget build(BuildContext context) {
final query = isar.users
.where()
.nameContains(searchQuery, caseSensitive: false)
.build();
return Column(
children: [
TextField(
onChanged: (value) {
setState(() {
searchQuery = value;
});
},
),
Expanded(
child: StreamBuilder<List<User>>(
stream: query.watch(fireImmediately: true),
builder: (context, snapshot) {
if (!snapshot.hasData) return SizedBox();
final users = snapshot.data!;
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
);
},
);
},
),
),
],
);
}
}Sync to Server
class SyncService {
void startWatching() {
isar.users.watchLazy().listen((_) async {
await syncUsersToServer();
});
}
Future<void> syncUsersToServer() async {
final users = await isar.users.where().findAll();
// Send to server...
}
}Cache Invalidation
class CacheService {
final _cache = <int, User>{};
void startWatching() {
isar.users.watchLazy().listen((_) {
_cache.clear();
print('Cache invalidated');
});
}
Future<User?> getUser(int id) async {
if (_cache.containsKey(id)) {
return _cache[id];
}
final user = await isar.users.get(id);
if (user != null) {
_cache[id] = user;
}
return user;
}
}Performance Considerations
// ✅ Use lazy watchers when you don't need data
isar.users.watchLazy().listen((_) {
// Just invalidate cache or flag for refresh
needsRefresh = true;
});
// ✅ Watch specific queries, not entire collections
isar.users
.where()
.statusEqualTo('active')
.watch()
.listen((activeUsers) {
// Handle active users
});
// ✅ Cancel subscriptions when done
final subscription = isar.users.watchLazy().listen((_) {});
// Later...
subscription.cancel();// ❌ Don't watch large collections with full data
isar.users.watch().listen((allUsers) {
// This refetches ALL users on ANY change
});
// ❌ Don't perform heavy operations in listener
isar.users.watchLazy().listen((_) async {
// Heavy operation
await processAllUsers();
});
// ❌ Don't forget to cancel subscriptions
isar.users.watchLazy().listen((_) {
// This keeps running forever if not cancelled
});Combining with StreamBuilder
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<List<User>>(
stream: isar.users
.where()
.ageGreaterThan(18)
.build()
.watch(fireImmediately: true),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Text('No users found');
}
final users = snapshot.data!;
return ListView(
children: users.map((user) =>
ListTile(title: Text(user.name))
).toList(),
);
},
);
}
}Best Practices
- Use Lazy Watchers when you only need notifications
- Watch Specific Queries instead of entire collections
- Cancel Subscriptions when widgets are disposed
- Avoid Heavy Operations in watcher callbacks
- Use fireImmediately for initial UI state
Watchers are efficient and lightweight. Use them freely to create reactive UIs!
Next Steps
Last Update