firewatch
Lightweight Firestore repositories for Flutter.
- π Auth-reactive: attach/detach on UID changes via any
ValueListenable<String?> - β‘ Instant UI: primes from local cache; then streams live updates
- πͺΆ Small surface area: single-doc, collection, and collection-group repos
- π Live window pagination: grow with
loadMore(), reset viaresetPages() - π§© No auth lock-in: bring your own auth listenable
Install
dependencies:
firewatch:
Quick start
Firewatch repositories are built to work seamlessly with watch_it.
Hereβs the minimal flow: Model β Repository β UI.
1. Define your model
Firestore-backed models must implement JsonModel so Firewatch can inject the
document ID.
class UserProfile implements JsonModel {
@override
final String id;
final String name;
UserProfile({required this.id, required this.name});
factory UserProfile.fromJson(Map<String, dynamic> json) => UserProfile(
id: json['id'] as String,
name: json['name'] as String? ?? 'Anonymous',
);
@override
Map<String, dynamic> toJson() => {'name': name};
}
2. Create your repositories
Repositories bind models to Firestore. Provide authUid
(a ValueListenable<String?>) so Firewatch knows which document/collection to
read.
final authUid = ValueNotifier<String?>(null); // wire this to your auth layer
class UserProfileRepository extends FirestoreDocRepository<UserProfile> {
UserProfileRepository()
: super(
fromJson: UserProfile.fromJson,
docRefBuilder: (fs, uid) => fs.doc('users/$uid'),
authUid: authUid,
);
}
class FriendsRepository extends FirestoreCollectionRepository<UserProfile> {
FriendsRepository()
: super(
fromJson: UserProfile.fromJson,
colRefBuilder: (fs, uid) => fs.collection('users/$uid/friends'),
authUid: authUid,
);
}
/// Query across ALL "friends" subcollections regardless of parent.
class AllFriendsRepository
extends FirestoreCollectionGroupRepository<UserProfile> {
AllFriendsRepository()
: super(
fromJson: UserProfile.fromJson,
queryRefBuilder: (fs, uid) => fs.collectionGroup('friends'),
authUid: authUid,
);
}
3. Consume in the UI with watch_it
Because repositories are ValueNotifiers, you can watch them directly in your widgets.
class ProfileCard extends StatelessWidget {
const ProfileCard({super.key});
@override
Widget build(BuildContext context) {
final profile = watchIt<UserProfileRepository>().value;
final friends = watchIt<FriendsRepository>().value;
if (profile == null) {
return const Center(child: CircularProgressIndicator());
}
return Card(
child: ListTile(
title: Text('User: ${profile.name}'),
subtitle: Text('Friends: ${friends.length}'),
),
);
}
}
π For a full runnable demo (with auth wiring and fake Firestore), check out the example/ app in this repo.
Parent ID injection
All repositories automatically inject parentId into the data map before
calling fromJson. This is the document ID of the parent document in the
Firestore path hierarchy. Models can opt-in by declaring a parentId field β
no changes to JsonModel required.
This is especially useful for collection group queries, where two documents
can share the same id but live under different parents
(users/u1/tasks/t1 vs projects/p1/tasks/t1).
class Task implements JsonModel {
@override
final String id;
final String title;
final String? parentId; // opt-in β injected automatically
Task({required this.id, required this.title, this.parentId});
factory Task.fromJson(Map<String, dynamic> json) => Task(
id: json['id'] as String,
title: json['title'] as String? ?? '',
parentId: json['parentId'] as String?,
);
@override
Map<String, dynamic> toJson() => {'title': title};
}
For a document at users/u1/tasks/t1, parentId will be 'u1'.
For a top-level document like users/u1, parentId will be null.
Bring your own Auth
Firewatch accepts any ValueListenable<String?> that yields the current user UID. Update it on sign-in/out and the repos will re-attach.
authUid.value = 'abc123'; // sign in
authUid.value = null; // sign out
Commands API
All repos expose command_it async
commands:
profileRepo.write(UserProfile(id: 'abc123', displayName: 'Marty'));
profileRepo.patch({'bio': 'Hello'});
friendsRepo.add({'displayName': 'Alice'});
friendsRepo.delete(id!);
// Collection-group writes use full document paths (no .add()):
allFriendsRepo.set((path: 'users/abc123/friends/f1', model: friend));
allFriendsRepo.patch((path: 'users/abc123/friends/f1', data: {'name': 'Bob'}));
allFriendsRepo.delete('users/abc123/friends/f1');
UI State
isLoading: true while fetching/refreshinghasInitialized(collections): first load completedhasMore(collections): whetherloadMore()can grow the windownotifierFor(docId): get a pre-soakedValueNotifier<T?>for a specific item (keyed by doc path for collection groups)
Documentation
License
MIT - See LICENSE
Libraries
- firewatch
- Firewatch β opinionated Firestore repositories for responsive UIs.