firestore_odm 2.7.1
firestore_odm: ^2.7.1 copied to clipboard
Type-safe Firestore ODM with code generation support. Generate type-safe Firestore operations with annotations.
Firestore ODM for Dart/Flutter #
Stop fighting with Firestore queries. Start building amazing apps.
Transform your Firestore development experience with type-safe, intuitive database operations that feel natural and productive.
๐ Complete Documentation #
๐ Read the Full Documentation - Comprehensive guides, examples, and API reference
๐ Table of Contents #
- Why Firestore ODM?
- Before vs After
- Key Features
- Quick Start
- Advanced Features
- Performance & Technical Excellence
- Testing
- Comparison with Standard Firestore
- Contributing
- License
๐ Why Firestore ODM? #
If you've worked with Flutter and Firestore, you know the pain:
- No Type Safety - String-based field paths that break at runtime, not compile time
- Manual Serialization - Converting
DocumentSnapshot
to models and back is tedious and error-prone - Complex Queries - Writing nested logical queries is difficult and hard to read
- Runtime Errors - Typos in field names cause crashes in production
- Incomplete Solutions - Other ODMs are often incomplete or not actively maintained
We built Firestore ODM to solve these problems with:
- โ Complete type safety throughout your entire data layer
- โ Lightning-fast code generation using callables and Dart extensions
- โ Minimal generated code that doesn't bloat your project
- โ Model reusability across collections and subcollections
- โ Revolutionary features like Smart Builder pagination and streaming aggregations
- โ Zero runtime overhead - all magic happens at compile time
๐ฅ Before vs After #
Type Safety Revolution #
// โ Standard cloud_firestore - Runtime errors waiting to happen
DocumentSnapshot doc = await FirebaseFirestore.instance
.collection('users')
.doc('user123')
.get();
Map<String, dynamic>? data = doc.data() as Map<String, dynamic>?;
String name = data?['name']; // Runtime error if field doesn't exist
int age = data?['profile']['age']; // Nested access is fragile
// โ
Firestore ODM - Compile-time safety
User? user = await db.users('user123').get();
String name = user.name; // IDE autocomplete, compile-time checking
int age = user.profile.age; // Type-safe nested access
Smart Query Building #
// โ Standard - String-based field paths, typos cause runtime errors
final result = await FirebaseFirestore.instance
.collection('users')
.where('isActive', isEqualTo: true)
.where('profile.followers', isGreaterThan: 100)
.where('age', isLessThan: 30)
.get();
// โ
ODM - Type-safe query builder with IDE support
final result = await db.users
.where(($) => $.and(
$.isActive(isEqualTo: true),
$.profile.followers(isGreaterThan: 100),
$.age(isLessThan: 30),
))
.get();
Intelligent Updates #
// โ Standard - Manual map construction, error-prone
await userDoc.update({
'profile.followers': FieldValue.increment(1),
'tags': FieldValue.arrayUnion(['verified']),
'lastLogin': FieldValue.serverTimestamp(),
});
// โ
ODM - Two smart update strategies
// 1. Patch - Explicit atomic operations (Best Performance)
await userDoc.patch(($) => [
$.profile.followers.increment(1),
$.tags.add('verified'), // Add single element
$.tags.addAll(['premium', 'active']), // Add multiple elements
$.scores.removeAll([0, -1]), // Remove multiple elements
$.lastLogin.serverTimestamp(),
]);
// 2. Modify - Smart diff with atomic operations (Convenient)
await userDoc.modify((user) => user.copyWith(
age: user.age + 1, // Auto-detects -> FieldValue.increment(1)
tags: [...user.tags, 'expert'], // Auto-detects -> FieldValue.arrayUnion()
lastLogin: FirestoreODM.serverTimestamp, // Server timestamp support
));
โก Key Features #
๐ก๏ธ Complete Type Safety #
- No
Map<String, dynamic>
anywhere in your code - Compile-time field validation - typos become build errors, not runtime crashes
- IDE autocomplete for all database operations
- Strong typing for nested objects and complex data structures
๐ Lightning Fast Code Generation #
- Highly optimized generated code using callables and Dart extensions
- Minimal output - smart generation without bloating your project
- Model reusability - same model works in collections and subcollections
- Zero runtime overhead - all magic happens at compile time
๐ง Revolutionary Pagination #
Our Smart Builder eliminates the most common Firestore pagination bugs:
// Get first page with ordering
final page1 = await db.users
.orderBy(($) => ($.followers(descending: true), $.name()))
.limit(10)
.get();
// Get next page with perfect type-safety - zero inconsistency risk
// The same orderBy ensures cursor consistency automatically
final page2 = await db.users
.orderBy(($) => ($.followers(descending: true), $.name()))
.startAfterObject(page1.last) // Auto-extracts cursor values
.limit(10)
.get();
๐ Streaming Aggregations (Unique Feature!) #
Real-time aggregation subscriptions that Firestore doesn't support natively:
// Live statistics that update in real-time
db.users
.where(($) => $.isActive(isEqualTo: true))
.aggregate(($) => (
count: $.count(),
averageAge: $.age.average(),
totalFollowers: $.profile.followers.sum(),
))
.stream
.listen((stats) {
print('Live: ${stats.count} users, avg age ${stats.averageAge}');
});
๐ฆ Smart Transactions #
Automatic deferred writes handle Firestore's read-before-write rule:
await db.runTransaction((tx) async {
// All reads happen first automatically
final sender = await tx.users('user1').get();
final receiver = await tx.users('user2').get();
// Writes are automatically deferred until the end
await tx.users('user1').patch(($) => [$.balance.increment(-100)]);
await tx.users('user2').patch(($) => [$.balance.increment(100)]);
});
โก Atomic Batch Operations #
Perform multiple writes atomically with two convenient approaches:
// Automatic management - simple and clean
await db.runBatch((batch) {
batch.users.insert(newUser);
batch.posts.update(existingPost);
batch.users('user_id').posts.insert(userPost);
batch.users('old_user').delete();
});
// Manual management - fine-grained control
final batch = db.batch();
batch.users.insert(user1);
batch.users.insert(user2);
batch.posts.update(post);
await batch.commit();
๐ Flexible Data Modeling #
Support for multiple modeling approaches:
freezed
(recommended) - Robust immutable classesjson_serializable
- Plain Dart classes with full controlfast_immutable_collections
- High-performanceIList
,IMap
,ISet
๐๏ธ Schema-Based Architecture #
- Multiple ODM instances for different app modules
- Compile-time validation of collection paths and relationships
- Automatic subcollection detection and type-safe access
- Clean separation of database concerns
๐ Quick Start #
1. Installation #
Install Firestore ODM:
dart pub add firestore_odm
dart pub add dev:firestore_odm_builder
dart pub add dev:build_runner
You'll also need a JSON serialization solution:
# If using Freezed
dart pub add freezed_annotation
dart pub add dev:freezed
dart pub add dev:json_serializable
# If using plain classes
dart pub add json_annotation
dart pub add dev:json_serializable
2. Configure json_serializable (Critical for Nested Models) #
โ ๏ธ Important: If you're using models with nested objects (especially with Freezed), you must create a build.yaml
file next to your pubspec.yaml
:
# build.yaml
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: true
Why is this required? Without this configuration, json_serializable
generates broken toJson()
methods for nested objects. Instead of proper JSON, you'll get Instance of 'NestedClass'
stored in Firestore, causing data corruption and deserialization failures.
When you need this:
- โ Using nested Freezed classes
- โ
Using nested objects with
json_serializable
- โ Working with complex object structures
- โ Encountering "Instance of..." in Firestore console
Alternative: Add @JsonSerializable(explicitToJson: true)
to individual classes if you can't use global configuration.
3. Define Your Model #
// lib/models/user.dart
import 'package:firestore_odm_annotation/firestore_odm_annotation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
@DocumentIdField() required String id,
required String name,
required String email,
required int age,
DateTime? lastLogin,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
4. Define Your Schema #
// lib/schema.dart
import 'package:firestore_odm_annotation/firestore_odm_annotation.dart';
import 'models/user.dart';
part 'schema.odm.dart';
@Schema()
@Collection<User>("users")
final appSchema = _$AppSchema;
5. Generate Code #
dart run build_runner build --delete-conflicting-outputs
6. Start Using #
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firestore_odm/firestore_odm.dart';
import 'schema.dart';
final firestore = FirebaseFirestore.instance;
final db = FirestoreODM(appSchema, firestore: firestore);
// Create a user with custom ID
await db.users.insert(User(
id: 'jane',
name: 'Jane Smith',
email: 'jane@example.com',
age: 28,
));
// Create a user with auto-generated ID
await db.users.insert(User(
id: FirestoreODM.autoGeneratedId,
name: 'John Doe',
email: 'john@example.com',
age: 30,
));
// Get a user
final user = await db.users('jane').get();
print(user?.name); // "Jane Smith"
// Type-safe queries
final youngUsers = await db.users
.where(($) => $.age(isLessThan: 30))
.orderBy(($) => $.name())
.get();
๐ Advanced Features #
Subcollections with Model Reusability #
@Schema()
@Collection<User>("users")
@Collection<Post>("posts")
@Collection<Post>("users/*/posts") // Same Post model, different location
final appSchema = _$AppSchema;
// Access user's posts
final userPosts = db.users('jane').posts;
await userPosts.insert(Post(id: 'post1', title: 'Hello World!'));
Bulk Operations #
// Update all premium users using patch (best performance)
await db.users
.where(($) => $.isPremium(isEqualTo: true))
.patch(($) => [$.points.increment(100)]);
// Update all premium users using modify (convenient but slower)
await db.users
.where(($) => $.isPremium(isEqualTo: true))
.modify((user) => user.copyWith(points: user.points + 100));
// Delete inactive users
await db.users
.where(($) => $.status(isEqualTo: 'inactive'))
.delete();
Server Timestamps & Auto-Generated IDs #
// Server timestamps using patch (best performance)
await userDoc.patch(($) => [$.lastLogin.serverTimestamp()]);
// Server timestamps using modify (convenient but slower)
await userDoc.modify((user) => user.copyWith(
lastLogin: FirestoreODM.serverTimestamp,
));
// โ ๏ธ IMPORTANT: Server timestamp arithmetic doesn't work
// โ This creates a regular DateTime, NOT a server timestamp:
// FirestoreODM.serverTimestamp + Duration(days: 1)
// Auto-generated document IDs
await db.users.insert(User(
id: FirestoreODM.autoGeneratedId, // Server generates unique ID
name: 'John Doe',
email: 'john@example.com',
));
โ ๏ธ Server Timestamp Warning: FirestoreODM.serverTimestamp
must be used exactly as-is. Any arithmetic operations (+
, .add()
, etc.) will create a regular DateTime
instead of a server timestamp. See the Server Timestamps Guide for alternatives.
๐ Performance & Technical Excellence #
Optimized Code Generation #
- Callables and Dart extensions for maximum performance
- Minimal generated code - no project bloat
- Compile-time optimizations - zero runtime overhead
- Smart caching and efficient build processes
Advanced Query Capabilities #
- Complex logical operations with
and()
andor()
- Array operations -
arrayContains
,arrayContainsAny
,whereIn
- Range queries with proper ordering constraints
- Nested field access with full type safety
Real-world Ready #
- Transaction support with automatic deferred writes
- Streaming subscriptions for real-time updates
- Error handling with meaningful compile-time messages
- Testing support with
fake_cloud_firestore
integration
๐งช Testing #
Perfect integration with fake_cloud_firestore
:
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('user operations work correctly', () async {
final firestore = FakeFirebaseFirestore();
final db = FirestoreODM(appSchema, firestore: firestore);
await db.users.insert(User(id: 'test', name: 'Test User', email: 'test@example.com', age: 25));
final user = await db.users('test').get();
expect(user?.name, 'Test User');
});
}
๐ Comparison with Standard Firestore #
Feature | Standard cloud_firestore | Firestore ODM |
---|---|---|
Type Safety | โ Map<String, dynamic> everywhere | โ Strong types throughout |
Query Building | โ String-based, error-prone | โ Type-safe with IDE support |
Data Updates | โ Manual map construction | โ Two smart update strategies |
Aggregations | โ Basic count only | โ Comprehensive + streaming |
Pagination | โ Manual, inconsistency risks | โ Smart Builder, zero risk |
Transactions | โ Manual read-before-write | โ Automatic deferred writes |
Code Generation | โ None | โ Highly optimized, minimal output |
Model Reusability | โ N/A | โ Same model, multiple collections |
Runtime Errors | โ Common | โ Eliminated at compile-time |
Developer Experience | โ Frustrating | โ Productive and enjoyable |
๐ค Contributing #
We love contributions! See our Contributing Guide for details.
๐ License #
MIT License - see LICENSE file for details.
Ready to transform your Firestore experience?
๐ Get Started Now | ๐ Full Documentation | ๐ Report Issues
Build type-safe, maintainable Flutter apps with the power of Firestore ODM! ๐