Firecraft: A Simplified Dart Wrapper for Cloud Firestore
Working with Cloud Firestore in Flutter applications often involves writing repetitive boilerplate code for common database operations. Enter Firecraft – a powerful Dart library that provides a clean, intuitive interface for interacting with Cloud Firestore, reducing complexity while maintaining full functionality.
What Problem Does Firecraft Solve?
When working directly with Cloud Firestore, developers often find themselves writing similar patterns over and over:
- Converting document snapshots to model objects
- Handling pagination logic
- Managing error handling and null safety
- Setting up real-time listeners
- Performing batch operations
Firecraft eliminates this repetition by providing a unified API that handles these common patterns while maintaining type safety and flexibility.
Key Features
Simplified CRUD Operations
Perform create, read, update, and delete operations with minimal code
Real-time Streaming
Built-in support for real-time document and collection updates
Smart Pagination
Advanced pagination with hasMore
indicators and cursor-based navigation
Batch Updates
Efficient batch operations for updating multiple documents
Document Counting
Real-time document counting with custom filtering
Type Safety
Full generic type support with custom JSON serialization
Getting Started
Firecraft uses the singleton pattern, so you can access it anywhere in your app:
Installation
Add Firecraft to your pubspec.yaml
:
dependencies:
firecraft:
Initialize the service:
final firecraft = Firecraft.instance;
Basic Operations
Fetching Documents
Single Document:
// Fetch a user by ID
final user = await firecraft.fetchDocument<User>(
documentPath: 'users/123',
fromJson: User.fromJson,
);
if (user != null) {
print('Found user: ${user.name}');
}
Collection:
// Fetch all users
final users = await firecraft.fetchCollection<User>(
collectionPath: 'users',
fromJson: User.fromJson,
);
// Fetch with custom query and limit
final activeUsers = await firecraft.fetchCollection<User>(
collectionPath: 'users',
fromJson: User.fromJson,
queryBuilder: (query) => query.where('isActive', isEqualTo: true),
limit: 10,
);
Adding and Updating Documents
Adding Documents:
// Add a new user
final docRef = await firecraft.addDocument(
collectionPath: 'users',
data: {
'name': 'John Doe',
'email': 'john@example.com',
'createdAt': FieldValue.serverTimestamp(),
},
);
print('New user ID: ${docRef.id}');
Setting Documents:
// Create or overwrite a document with specific ID
await firecraft.setDocument(
collectionPath: 'users',
docId: 'user_123',
data: {
'name': 'Jane Smith',
'email': 'jane@example.com',
},
);
Updating Documents:
// Update specific fields
await firecraft.updateDocument(
documentPath: 'users/123',
data: {
'lastLogin': FieldValue.serverTimestamp(),
'loginCount': FieldValue.increment(1),
},
);
Deleting Documents
await firecraft.deleteDocument(documentPath: 'users/123');
Real-time Streaming
One of Firecraft’s most powerful features is its built-in streaming capabilities.
Document Streaming
// Stream a single document
firecraft.streamDocument<User>(
documentPath: 'users/123',
fromJson: User.fromJson,
).listen((user) {
if (user != null) {
print('User updated: ${user.name}');
} else {
print('User deleted or does not exist');
}
});
Field-specific Streaming
// Stream only a specific field
firecraft.streamDocumentField<bool>(
documentPath: 'users/123',
key: 'isOnline',
).listen((isOnline) {
print('User online status: $isOnline');
});
Collection Streaming
// Stream an entire collection with real-time updates
firecraft.streamCollection<User>(
collectionPath: 'users',
fromJson: User.fromJson,
queryBuilder: (query) => query.where('isActive', isEqualTo: true),
).listen((users) {
print('Active users count: ${users.length}');
});
Advanced Features
Pagination Made Easy
Firecraft provides sophisticated pagination support that handles cursor-based navigation automatically.
Initial Page:
final firstPage = await firecraft.fetchInitialPage<Post>(
collectionPath: 'posts',
fromJson: Post.fromJson,
queryBuilder: (query) => query.orderBy('createdAt', descending: true),
limit: 20,
);
print('Posts: ${firstPage.items.length}');
print('Has more: ${firstPage.hasMore}');
Next Page:
if (firstPage.hasMore && firstPage.lastDocument != null) {
final nextPage = await firecraft.fetchNextPage<Post>(
collectionPath: 'posts',
fromJson: Post.fromJson,
lastDocument: firstPage.lastDocument!,
queryBuilder: (query) => query.orderBy('createdAt', descending: true),
limit: 20,
);
}
Real-time Paginated Streaming:
firecraft.paginatedCollection<Post>(
collectionPath: 'posts',
fromJson: Post.fromJson,
queryBuilder: (query) => query.orderBy('createdAt', descending: true),
limit: 20,
).listen((result) {
print('Page items: ${result.items.length}');
print('Has more: ${result.hasMore}');
});
Batch Updates with Conditions
Update multiple documents efficiently based on custom conditions:
final updatedCount = await firecraft.updateWhereField(
collectionPath: 'users',
fieldToUpdate: 'status',
newValue: 'inactive',
condition: (data) {
final lastLogin = data['lastLogin'] as Timestamp?;
if (lastLogin == null) return true;
final daysSinceLogin = DateTime.now()
.difference(lastLogin.toDate())
.inDays;
return daysSinceLogin > 30; // Inactive for 30+ days
},
queryBuilder: (query) => query.where('status', isEqualTo: 'active'),
);
print('Updated $updatedCount users to inactive status');
Real-time Document Counting
// Count all active users
firecraft.streamDocumentCount(
collectionPath: 'users',
queryBuilder: (query) => query.where('isActive', isEqualTo: true),
).listen((count) {
print('Active users: $count');
});
// Count with additional filtering
firecraft.streamDocumentCount(
collectionPath: 'posts',
additionalCondition: (data) {
final content = data['content'] as String?;
return content != null && content.length > 100;
},
).listen((count) {
print('Long posts: $count');
});
Error Handling
Firecraft automatically catches and rethrows FirebaseException
s, allowing you to handle them appropriately:
try {
final user = await firecraft.fetchDocument<User>(
documentPath: 'users/123',
fromJson: User.fromJson,
);
} on FirebaseException catch (e) {
print('Firestore error: ${e.message}');
// Handle specific error cases
switch (e.code) {
case 'permission-denied':
// Handle permission error
break;
case 'unavailable':
// Handle network error
break;
}
}
Best Practices
1. Model Classes with fromJson
Create model classes with fromJson
constructors for type safety:
class User {
final String id;
final String name;
final String email;
final DateTime? createdAt;
User({
required this.id,
required this.name,
required this.email,
this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
createdAt: json['createdAt']?.toDate(),
);
}
}
2. Use Query Builders for Complex Queries
Leverage the queryBuilder
parameter for complex filtering:
final premiumActiveUsers = await firecraft.fetchCollection<User>(
collectionPath: 'users',
fromJson: User.fromJson,
queryBuilder: (query) => query
.where('isPremium', isEqualTo: true)
.where('isActive', isEqualTo: true)
.orderBy('createdAt', descending: true),
);
3. Dispose of Stream Subscriptions
Always dispose of stream subscriptions to prevent memory leaks:
class UserService {
StreamSubscription<User?>? _userSubscription;
void startListening(String userId) {
_userSubscription = firecraft.streamDocument<User>(
documentPath: 'users/$userId',
fromJson: User.fromJson,
).listen((user) {
// Handle user updates
});
}
void dispose() {
_userSubscription?.cancel();
}
}
Conclusion
Firecraft transforms the way you interact with Cloud Firestore by providing a clean, intuitive API that handles common patterns while maintaining the full power and flexibility of Firestore. Whether you’re building a simple app or a complex application with real-time features, Firecraft reduces boilerplate code and helps you focus on what matters most – building great user experiences.
The library’s type-safe approach, combined with powerful features like real-time streaming, smart pagination, and batch operations, makes it an excellent choice for Flutter developers working with Firestore.
Try Firecraft in your next Flutter project and experience the difference a well-designed abstraction can make!