firewatch 1.3.1 copy "firewatch: ^1.3.1" to clipboard
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 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

1
likes
160
points
506
downloads

Publisher

unverified uploader

Weekly Downloads

Lightweight Firestore repositories for Flutter: single-doc, collection, and collection-group repos that react to auth, stream updates, and support live-window pagination.

Repository (GitHub)
View/report issues

Topics

#firestore #repository #state-management #pagination #flutter

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

cloud_firestore, command_it, flutter

More

Packages that depend on firewatch