firebase_query_builder

pub package likes popularity pub points

A fluent, chainable Firestore query builder for Flutter. Write clean, readable, and expressive Firestore queries — no more deeply nested method chains.


The Problem

Firestore queries in Flutter get messy fast:

// Standard Firestore — hard to read, easy to make mistakes
FirebaseFirestore.instance
    .collection('orders')
    .where('status', isEqualTo: 'pending')
    .where('amount', isGreaterThan: 100)
    .where('tags', arrayContains: 'priority')
    .orderBy('createdAt', descending: true)
    .limit(20)
    .get();

The Solution

// firebase_query_builder — clean, readable, expressive
FireQuery.from('orders')
    .where('status').equals('pending')
    .where('amount').greaterThan(100)
    .where('tags').contains('priority')
    .orderBy('createdAt', descending: true)
    .limit(20)
    .fetch();

Features

  • ✅ Fluent, chainable API for all Firestore filter types
  • ✅ Built-in cursor-based pagination with FireQueryPaginator
  • Real-time streaming with .stream()
  • Single document retrieval with .fetchOne()
  • Count documents efficiently without fetching with .count()
  • Typed mapping with .mapTo<T>()
  • ✅ String prefix search with .startsWith()
  • ✅ Helpful error messages via FireQueryException
  • ✅ Full test coverage

Installation

Add to your pubspec.yaml:

dependencies:
  firebase_query_builder: ^0.0.1

Then run:

flutter pub get

Usage

Basic Fetch

import 'package:firebase_query_builder/firebase_query_builder.dart';

final result = await FireQuery.from('products')
    .where('category').equals('electronics')
    .where('price').lessThan(1000)
    .orderBy('price')
    .limit(10)
    .fetch();

print('Found ${result.count} products');

for (final doc in result.docs) {
  print(doc.data());
}

Fetch a Single Document

final doc = await FireQuery.from('users')
    .where('email').equals('user@example.com')
    .fetchOne();

if (doc != null) {
  print('Found: ${doc.data()['name']}');
}

Real-Time Stream

FireQuery.from('messages')
    .where('roomId').equals('room_001')
    .orderBy('sentAt', descending: true)
    .limit(50)
    .stream()
    .listen((result) {
  print('${result.count} messages');
});

Pagination

final paginator = FireQuery.from('orders')
    .where('status').equals('pending')
    .orderBy('createdAt', descending: true)
    .paginate(pageSize: 10);

// Load page by page
while (paginator.hasMore) {
  final page = await paginator.nextPage();
  if (page == null) break;
  print('Page ${paginator.currentPage}: ${page.count} orders');
}

// Or fetch everything at once
final all = await paginator.fetchAll();

Count Without Fetching

// Efficient — does not download documents
final count = await FireQuery.from('orders')
    .where('status').whereIn(['pending', 'processing'])
    .count();

print('Active orders: $count');

Typed Mapping

final result = await FireQuery.from('users').fetch();

final users = result.mapTo((doc) => User.fromMap(doc.data()));

Collection Groups

final result = await FireQuery.fromGroup('comments')
    .where('userId').equals('abc123')
    .orderBy('createdAt', descending: true)
    .fetch();

Available Filter Methods

Method Description
.equals(value) Field equals value
.notEquals(value) Field does not equal value
.lessThan(value) Field < value
.lessThanOrEqual(value) Field <= value
.greaterThan(value) Field > value
.greaterThanOrEqual(value) Field >= value
.isNull() Field is null
.isNotNull() Field is not null
.contains(value) Array field contains value
.containsAny(values) Array field contains any of values
.whereIn(values) Field is one of values
.whereNotIn(values) Field is not one of values
.startsWith(prefix) String field starts with prefix

FireQueryResult Helpers

final result = await FireQuery.from('orders').fetch();

result.count        // number of documents
result.isEmpty      // true if no documents
result.isNotEmpty   // true if has documents
result.docs         // List<QueryDocumentSnapshot>
result.data         // List<Map<String, dynamic>>
result.ids          // List of document IDs
result.firstDocument  // first document or null
result.lastDocument   // last document or null (useful for cursors)
result.mapTo((doc) => MyModel.fromMap(doc.data()))
result.raw          // original QuerySnapshot

Error Handling

try {
  final result = await FireQuery.from('orders').fetch();
} on FireQueryException catch (e) {
  print('Query failed: ${e.message}');
  print('Caused by: ${e.cause}');
}

Requirements

  • Flutter >= 3.10.0
  • Dart >= 3.0.0
  • cloud_firestore >= 5.0.0

Contributing

Contributions are welcome! Please open an issue or pull request on GitHub.


License

MIT — see LICENSE

Libraries

firebase_query_builder
A fluent, chainable Firestore query builder for Flutter.