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 via resetPages()
  • 🧩 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/refreshing
  • hasInitialized (collections): first load completed
  • hasMore (collections): whether loadMore() can grow the window
  • notifierFor(docId): get a pre-soaked ValueNotifier<T?> for a specific item (keyed by doc path for collection groups)

Documentation

API Docs

License

MIT - See LICENSE

Libraries

firewatch
Firewatch – opinionated Firestore repositories for responsive UIs.