firewatch 1.3.1
firewatch: ^1.3.1 copied to clipboard
Lightweight Firestore repositories for Flutter: single-doc, collection, and collection-group repos that react to auth, stream updates, and support live-window pagination.
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