A powerful, type-safe wrapper around Cloud Firestore operations for Flutter applications. This package provides a robust foundation for building scalable applications with Firestore, offering automatic type conversion, state management, and enhanced error handling.
-
- Automatic type conversion between Firestore and Dart objects
- Built-in validation through
TurboWriteable
- Local ID and reference field management
-
- Optimistic UI updates with rollback
- Local state synchronization
- Automatic stream update blocking during mutations
- Authentication state synchronization
-
- Batch operations support
- Transaction support for atomic operations
- Collection group queries
- Real-time data streaming
- Search capabilities
-
- Debouncing support
- Mutex operations
- Optimistic updates
- Automatic timestamp management
-
- Comprehensive error handling
- Detailed logging system
- Sensitive data protection
- Operation validation
- Create an API instance:
final api = TurboFirestoreApi<User>(
firebaseFirestore: FirebaseFirestore.instance,
collectionPath: () => 'users',
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
- Create a service:
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
Future<TurboResponse<DocumentReference>> createUser({
required String name,
required int age,
}) async {
final user = UserDto.defaultValue(
id: api.genId,
userId: currentUser.id,
name: name,
age: age,
);
return createDoc(doc: user);
}
}
That's it! You can now use all the features of Turbo Firestore API in your application.
Turbo Firestore API ensures type safety throughout your Firestore interactions, providing a seamless and error-free development experience.
The package automatically converts between Firestore documents and Dart objects, eliminating the need for manual serialization and deserialization.
final api = TurboFirestoreApi<User>(
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
Turbo Firestore API includes built-in validation through the TurboWriteable
abstract class, ensuring data integrity and consistency. By extending TurboWriteable
, you can implement custom validation logic, handle field-level checks, and ensure data meets your application's requirements.
import 'package:turbo_firestore_api/abstracts/turbo_writeable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:turbo_response/turbo_response.dart';
part 'user.g.dart';
@JsonSerializable(includeIfNull: false, explicitToJson: true)
class User extends TurboWriteable {
/// User's unique identifier (managed by Firestore API)
@JsonKey(ignore: true)
String? id;
/// User's full name
final String name;
/// User's age
final int age;
/// User's email address
final String email;
User({
this.id,
required this.name,
required this.age,
required this.email,
});
/// Validation method to ensure data integrity
/// Returns null if validation passes, or a TurboResponse with error details if validation fails
@override
TurboResponse<void>? validate() {
// Validate name
if (name.isEmpty) {
return TurboResponse.fail(
error: Exception('Name cannot be empty'),
title: 'Invalid Name',
message: 'Name cannot be empty',
);
}
if (name.length < 2) {
return TurboResponse.fail(
error: Exception('Name too short'),
title: 'Invalid Name',
message: 'Name must be at least 2 characters long',
);
}
// Validate age
if (age < 0) {
return TurboResponse.fail(
error: Exception('Invalid age'),
title: 'Invalid Age',
message: 'Age must be non-negative',
);
}
if (age > 120) {
return TurboResponse.fail(
error: Exception('Unrealistic age'),
title: 'Invalid Age',
message: 'Age seems unrealistic',
);
}
// Validate email
final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!emailRegex.hasMatch(email)) {
return TurboResponse.fail(
error: Exception('Invalid email format'),
title: 'Invalid Email',
message: 'Invalid email format',
);
}
// Return null if all validations pass
return null;
}
/// Convert to JSON, automatically excluding ID and other transient fields
@override
Map<String, dynamic> toJson() => _$UserToJson(this);
/// Create from JSON, allowing package to inject ID if needed
factory User.fromJson(Map<String, dynamic> json) {
final user = _$UserFromJson(json);
return user;
}
}
- Comprehensive Validation: The
validate()
method performs multiple checks on different fields and returnsnull
if all validations pass. - TurboResponse Integration: Returns
TurboResponse.fail()
with detailed error information when validation fails. - JSON Serialization: Uses
json_annotation
to control serialization. - ID Management: Demonstrates how the package handles document IDs.
final api = TurboFirestoreApi<User>(
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
);
// Validation happens automatically during create/update operations
final response = await api.createDoc(doc: user);
response.fold(
ifSuccess: (documentReference) {
print('User created successfully');
},
orElse: (error) {
print('What went wrong: ${error.message}');
},
);
- Proactive Data Validation: Catch data inconsistencies before they reach Firestore
- Flexible Validation Logic: Implement custom validation rules specific to your models
- Automatic Error Handling: Seamless integration with Turbo Firestore API's error management
- Type-Safe Operations: Ensure data integrity at compile-time and runtime
The package simplifies local ID and DocumentReference
field management, making it easy to work with document relationships and identifiers.
import 'package:json_annotation/json_annotation.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:turbo_firestore_api/abstracts/turbo_writeable.dart';
part 'user.g.dart';
@JsonSerializable(includeIfNull: false, explicitToJson: true)
class User extends TurboWriteable {
/// Document ID - included when reading from JSON, excluded when writing to JSON
@JsonKey(includeFromJson: true, includeToJson: false)
final String id;
/// Document reference - included when reading from JSON, excluded when writing to JSON
@JsonKey(includeFromJson: true, includeToJson: false)
final DocumentReference documentReference;
/// User's name
final String name;
/// User's age
final int age;
User({
required this.id,
required this.documentReference,
required this.name,
required this.age,
});
/// Convert to JSON - package automatically handles id and documentReference
@override
Map<String, dynamic> toJson() => _$UserToJson(this);
/// Create from JSON - package automatically injects id and documentReference
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// Configure API to handle ID and reference fields
final api = TurboFirestoreApi<User>(
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
tryAddLocalId: true, // Automatically adds document ID to model
idFieldName: 'id', // Field name for ID in your model
tryAddLocalDocumentReference: true, // Automatically adds document reference to model
documentReferenceFieldName: 'documentReference', // Field name for reference in your model
);
// When reading, the package automatically adds ID and reference:
final response = await api.getDoc(id: 'user123');
response.fold(
ifSuccess: (user) {
print('User ID: ${user.id}'); // 'user123'
print('User Ref: ${user.documentReference.path}'); // 'users/user123'
},
orElse: (error) => print('Error: ${error.message}'),
);
// When writing, the package automatically handles ID and reference:
final newUser = User(
id: api.genId,
documentReference: api.getDocRefById(id: api.genId),
name: 'John',
age: 30,
);
await api.createDoc(doc: newUser);
// The package automatically excludes id and documentReference when writing to Firestore
The package handles ID and reference fields by:
- Reading: Automatically injects the document ID and reference into your model
- Writing: Automatically excludes these fields when writing to Firestore
- Type Safety: Uses your model's field types for proper type checking
- Flexibility: Configurable field names to match your model structure
Turbo Firestore API provides robust state management capabilities, ensuring a smooth and responsive user experience.
The package supports optimistic UI updates with automatic rollback, providing instant feedback to users while maintaining data consistency.
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
/// Updates a user's name with optimistic UI update
Future<TurboResponse<DocumentReference>> updateUserName(User user, String newName) async {
// Create updated user
final updatedUser = User(
id: user.id,
documentReference: user.documentReference,
name: newName,
age: user.age,
);
// Update local state immediately and sync with Firestore
return updateDoc(
doc: updatedUser,
doNotifyListeners: true, // Notify listeners of local state change
);
}
}
// Usage in UI
class UserNameField extends StatelessWidget {
final User user;
final UsersService service;
@override
Widget build(BuildContext context) {
return TextField(
initialValue: user.name,
onSubmitted: (newName) async {
// UI updates immediately due to optimistic update
final response = await service.updateUserName(user, newName);
response.fold(
ifSuccess: (_) {
// Update successful, local state already reflects change
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Name updated successfully')),
);
},
orElse: (error) {
// Update failed, but local state was already updated
// Service maintains consistency
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update name: ${error.message}')),
);
},
);
},
);
}
}
Key features of optimistic updates:
- Instant UI Updates: Local state is updated immediately before Firestore operation
- Automatic Stream Blocking: Prevents stream updates during mutations to avoid UI flicker
- Error Handling: Maintains consistent state even if remote update fails
- Transaction Support: Can be used within transactions for atomic operations
- Batch Operations: Supports optimistic updates for multiple documents
The package provides powerful local state synchronization with hooks for pre-processing data before state updates:
class UsersService extends BeTurboCollectionService<User, UsersApi> {
UsersService({required super.api});
// Maintain a sorted list of users by age
List<User> _sortedUsers = [];
List<User> get sortedUsers => _sortedUsers;
// Called before local state updates
@override
void beforeSyncNotifyUpdate(List<User> docs) {
// Sort users by age before updating local state
_sortedUsers = List<User>.from(docs)
..sort((a, b) => b.age.compareTo(a.age));
}
// Direct access by ID for efficient lookups
User? getUserById(String id) => tryFindById(id);
// Check if user exists
bool hasUser(String id) => exists(id);
}
// Usage in UI - Sorted List
class UsersByAgeList extends StatelessWidget {
final UsersService service;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: service.listenable,
builder: (context, child) {
final sortedUsers = service.sortedUsers;
if (sortedUsers.isEmpty) {
return Text('No users found');
}
return ListView.builder(
itemCount: sortedUsers.length,
itemBuilder: (context, index) {
final user = sortedUsers[index];
return ListTile(
title: Text(user.name),
trailing: Text('Age: ${user.age}'),
);
},
);
},
);
}
}
// Usage in UI - Direct Access
class UserProfile extends StatelessWidget {
final UsersService service;
final String userId;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: service.listenable,
builder: (context, child) {
final user = service.getUserById(userId);
if (user == null) {
return Text('User not found');
}
return Column(
children: [
Text('Name: ${user.name}'),
Text('Age: ${user.age}'),
],
);
},
);
}
}
Key features of local state synchronization:
- Pre-processing Hooks: Process data before state updates with
beforeSyncNotifyUpdate
- Efficient Access: Direct access by ID through
tryFindById
- Derived State: Maintain sorted or filtered views of the data
- Real-time Updates: All views stay in sync with Firestore changes
- Type Safety: Full type safety for all operations
The package automatically blocks stream updates during mutations to prevent race conditions and UI flicker. This is especially important for optimistic updates:
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
/// Updates multiple user ages in a batch
Future<TurboResponse> updateUserAges(List<User> users, int newAge) async {
final updatedUsers = users.map((user) => User(
id: user.id,
documentReference: user.documentReference,
name: user.name,
age: newAge,
)).toList();
// During this batch update:
// 1. Local state updates immediately
// 2. Stream updates are blocked
// 3. Remote update executes
// 4. Stream unblocks after completion
return updateDocs(
docs: updatedUsers,
doNotifyListeners: true,
);
}
}
// Usage in UI
class UserAgeUpdateButton extends StatelessWidget {
final UsersService service;
final List<User> selectedUsers;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
// UI updates immediately due to optimistic update
// Stream updates are blocked until operation completes
final response = await service.updateUserAges(selectedUsers, 25);
response.fold(
ifSuccess: (_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ages updated successfully')),
);
},
orElse: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to update ages: ${error.message}')),
);
},
);
},
child: Text('Set Age to 25'),
);
}
}
Key features of stream update blocking:
- Race Condition Prevention: Prevents stream updates from overwriting optimistic updates
- UI Consistency: Eliminates UI flicker during mutations
- Automatic Timing: Blocks only for the duration of the mutation
- Batch Support: Works with single operations and batch updates
- Transaction Support: Compatible with Firestore transactions
The package automatically manages state based on Firebase Authentication:
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
// Handle data updates and auth state changes
@override
void Function(List<User>? value, User? user) get onData {
return (value, user) {
if (user != null) {
// User is signed in, update local state
final docs = value ?? [];
log.debug('Updating ${docs.length} docs for user ${user.uid}');
_docsPerId.update(docs.toIdMap((element) => element.id));
_isReady.completeIfNotComplete();
} else {
// User is signed out, clear local state
log.debug('User is null, clearing all docs');
_docsPerId.update({});
}
};
}
// Access current user's ID (null if signed out)
String? get currentUserId => cachedUserId;
}
Key features of authentication state synchronization:
- Automatic State Management:
- Updates local state when user signs in
- Clears local state when user signs out
- Efficient Data Handling:
- Maintains data in indexed map structure
- Provides easy access to current user ID
- UI Integration:
- Automatic UI updates on auth state changes
- Clean handling of signed-out state
- Resource Management:
- Proper cleanup of subscriptions
- Memory leak prevention
Turbo Firestore API offers a wide range of advanced operations, enabling complex and efficient data management.
The package supports batch operations with optimistic updates, allowing you to perform multiple writes atomically while maintaining UI responsiveness:
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
/// Deactivates multiple users in a single atomic operation
Future<TurboResponse> deactivateUsers(List<User> users) async {
// Create updated users with deactivated status
final deactivatedUsers = users.map((user) => User(
id: user.id,
documentReference: user.documentReference,
name: user.name,
age: user.age,
isActive: false,
)).toList();
// During this batch update:
// 1. Local state updates immediately for all users
// 2. Stream updates are blocked
// 3. All updates are added to a batch
// 4. Batch is committed atomically
// 5. Stream unblocks after completion
return updateDocs(
docs: deactivatedUsers,
doNotifyListeners: true,
);
}
}
Key features of batch operations:
- Atomic Updates: All operations succeed or fail together
- Optimistic UI: Local state updates immediately for better UX
- Stream Blocking: Prevents stream updates during batch operation
- Type Safety: Full type safety for all batch operations
The package supports transactions with optimistic updates, allowing you to perform atomic operations while maintaining UI responsiveness:
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
/// Transfer points between users atomically
Future<TurboResponse> transferPoints({
required User fromUser,
required User toUser,
required int points,
}) async {
final updatedFromUser = User(
id: fromUser.id,
documentReference: fromUser.documentReference,
name: fromUser.name,
points: fromUser.points - points,
);
final updatedToUser = User(
id: toUser.id,
documentReference: toUser.documentReference,
name: toUser.name,
points: toUser.points + points,
);
// In transactions, methods automatically throw on failure to abort the transaction
return api.runTransaction((transaction) async {
// Example of throwing inside transaction based on a condition
if (fromUser.points < points) {
TurboResponse.throwException(
error: 'Insufficient points',
message: 'User does not have enough points for transfer',
);
}
await updateDoc(
doc: updatedFromUser,
transaction: transaction,
);
await updateDoc(
doc: updatedToUser,
transaction: transaction,
);
return TurboResponse.emptySuccess();
});
}
}
Key features of transactions:
- Atomic Operations: All operations succeed or fail together
- Optimistic UI: Local state updates immediately for better UX
- Fail-Fast: Uses
tryThrowFail
to immediately abort failed transactions - Type Safety: Full type safety for all operations
- Error Handling: Automatic rollback on failure
The package enables querying across multiple collections with the same name, regardless of their location in the document hierarchy. This is particularly useful for hierarchical data structures:
class CommentsService extends TurboCollectionService<Comment, CommentsApi> {
CommentsService({required super.api});
/// Finds all comments across the entire app, regardless of parent document
Future<TurboResponse<List<Comment>>> findAllComments() async {
return api.listByQueryWithConverter(
collectionReferenceQuery: (ref) => ref
.orderBy('createdAt', descending: true)
.limit(50),
whereDescription: 'Finding recent comments across all collections',
);
}
/// Finds comments by a specific user across all collections
Future<TurboResponse<List<Comment>>> findUserComments(String userId) async {
return api.listByQueryWithConverter(
collectionReferenceQuery: (ref) => ref
.where('userId', isEqualTo: userId)
.orderBy('createdAt', descending: true),
whereDescription: 'Finding user comments across all collections',
);
}
}
// Configure API to use collection group
final api = TurboFirestoreApi<Comment>(
firebaseFirestore: FirebaseFirestore.instance,
collectionPath: () => 'comments', // Collection name to query across all paths
fromJson: Comment.fromJson,
toJson: (comment) => comment.toJson(),
isCollectionGroup: true, // Enable collection group queries
);
// Usage in UI
class RecentCommentsView extends StatelessWidget {
final CommentsService service;
@override
Widget build(BuildContext context) {
return FutureBuilder<TurboResponse<List<Comment>>>(
future: service.findAllComments(),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return snapshot.data!.fold(
ifSuccess: (comments) => ListView.builder(
itemCount: comments.length,
itemBuilder: (context, index) {
final comment = comments[index];
return ListTile(
title: Text(comment.text),
subtitle: Text('By: ${comment.userName}'),
);
},
),
orElse: (error) => Text('Error: ${error.message}'),
);
},
);
}
}
Key features of collection group queries:
- Hierarchical Data: Query same-named collections at any path depth
- Type Safety: Full type conversion and validation
- Query Support: All standard query operations (where, orderBy, limit)
- Performance: Efficient querying across multiple collections
- Error Handling: Proper error handling and logging
The package provides built-in real-time data streaming through TurboCollectionService
, with automatic state management and UI synchronization:
class UsersService extends BeTurboCollectionService<User, UsersApi> {
UsersService({required super.api});
// Maintain a sorted list of active users
List<User> _activeUsers = [];
List<User> get activeUsers => _activeUsers;
// Process data before notifying listeners
@override
void beforeSyncNotifyUpdate(List<User> docs) {
_activeUsers = docs
.where((user) => user.isActive)
.toList()
..sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
}
}
// Usage in UI with automatic state management
class ActiveUsersView extends StatelessWidget {
const ActiveUsersView({
super.key,
required this.service,
required this.viewModel,
});
final UsersService service;
final UsersViewModel viewModel;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: service.listenable,
builder: (context, _) {
final users = service.activeUsers;
if (users.isEmpty) {
return Text('No active users');
}
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text('Last seen: ${user.lastSeen}'),
trailing: OnlineIndicator(user: user),
onTap: () => viewModel.onUserPressed(user.id),
);
},
);
},
);
}
}
// View model for handling user interactions
class UsersViewModel extends ChangeNotifier {
final UsersService _service;
UsersViewModel({required UsersService service}) : _service = service;
User? _selectedUser;
User? get selectedUser => _selectedUser;
void onUserPressed(String userId) {
// Direct access by ID, no need for additional queries
_selectedUser = _service.findById(userId);
notifyListeners();
}
}
// Selected user details view
class SelectedUserView extends StatelessWidget {
final UsersViewModel viewModel;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, _) {
final user = viewModel.selectedUser;
if (user == null) {
return Text('No user selected');
}
return Column(
children: [
Text('Name: ${user.name}'),
Text('Status: ${user.isActive ? "Online" : "Offline"}'),
Text('Last seen: ${user.lastSeen}'),
],
);
},
);
}
}
Key features of real-time streaming:
- Built-in State Management: Service handles all streaming and state updates
- Pre-processing: Use
BeTurboCollectionService
to process data before updates - Efficient Updates: Only rebuilds UI when data actually changes
- Direct Access: Fast lookups using
findById
without additional queries - Memory Efficient: Automatic cleanup of streams and subscriptions
The package provides powerful search capabilities with support for both text and numeric searches:
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
/// Search users by name prefix
Future<TurboResponse<List<User>>> searchByName(String namePrefix) {
return api.listBySearchTermWithConverter(
searchTerm: namePrefix,
searchField: 'name',
searchTermType: TurboSearchTermType.startsWith,
limit: 20, // Optional limit
);
}
/// Search users by skills (array field)
Future<TurboResponse<List<User>>> searchBySkills(String skill) {
return api.listBySearchTermWithConverter(
searchTerm: skill,
searchField: 'skills',
searchTermType: TurboSearchTermType.arrayContains,
);
}
/// Search by age with automatic number conversion
Future<TurboResponse<List<User>>> searchByAge(String ageInput) {
return api.listBySearchTermWithConverter(
searchTerm: ageInput,
searchField: 'age',
searchTermType: TurboSearchTermType.startsWith,
doSearchNumberEquivalent: true, // Will also try to match numeric value
);
}
}
// View model for search functionality
class SearchViewModel extends ChangeNotifier {
final UsersService _service;
SearchViewModel({required UsersService service}) : _service = service;
List<User> _searchResults = [];
List<User> get searchResults => _searchResults;
bool _isLoading = false;
bool get isLoading => _isLoading;
String _error = '';
String get error => _error;
Future<void> searchUsers(String query) async {
if (query.isEmpty) {
_searchResults = [];
_error = '';
notifyListeners();
return;
}
_isLoading = true;
_error = '';
notifyListeners();
try {
// Try name search first
final response = await _service.searchByName(query);
response.fold(
ifSuccess: (users) {
_searchResults = users;
_error = '';
},
orElse: (error) {
_searchResults = [];
_error = error.message ?? 'Search failed';
},
);
} finally {
_isLoading = false;
notifyListeners();
}
}
}
// Usage in UI
class UserSearchView extends StatelessWidget {
final SearchViewModel viewModel;
@override
Widget build(BuildContext context) {
return Column(
children: [
SearchBar(
onChanged: (query) => viewModel.searchUsers(query),
),
ListenableBuilder(
listenable: viewModel,
builder: (context, _) {
if (viewModel.isLoading) {
return CircularProgressIndicator();
}
if (viewModel.error.isNotEmpty) {
return Text('Error: ${viewModel.error}');
}
final results = viewModel.searchResults;
if (results.isEmpty) {
return Text('No results found');
}
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
final user = results[index];
return ListTile(
title: Text(user.name),
subtitle: Text('Age: ${user.age}'),
trailing: Wrap(
children: user.skills.map((skill) =>
Chip(label: Text(skill))
).toList(),
),
);
},
);
},
),
],
);
}
}
Key features of search capabilities:
- Multiple Search Types: Support for prefix matching and array containment
- Numeric Search: Automatic handling of numeric values
- Type Safety: Full type conversion and validation
- Result Limiting: Optional limit for large result sets
- Error Handling: Proper error handling with
TurboResponse
Turbo Firestore API automatically manages timestamp fields, simplifying document tracking:
// Configure API with timestamp fields
final api = TurboFirestoreApi<User>(
firebaseFirestore: FirebaseFirestore.instance,
collectionPath: () => 'users',
fromJson: User.fromJson,
toJson: (user) => user.toJson(),
// Enable automatic timestamp management
createdAtFieldName: 'createdAt',
updatedAtFieldName: 'updatedAt',
);
// User model with timestamp fields
class User extends TurboWriteable {
final String id;
final DocumentReference? documentReference;
final String name;
final int age;
final DateTime? createdAt;
final DateTime? updatedAt;
User({
required this.id,
this.documentReference,
required this.name,
required this.age,
this.createdAt,
this.updatedAt,
});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as String,
documentReference: json['documentReference'] as DocumentReference?,
name: json['name'] as String,
age: json['age'] as int,
createdAt: (json['createdAt'] as Timestamp?)?.toDate(),
updatedAt: (json['updatedAt'] as Timestamp?)?.toDate(),
);
}
@override
Map<String, dynamic> toJson() {
return {
'name': name,
'age': age,
// Timestamps are handled automatically by the API
};
}
@override
TurboResponse? validate() => null; // No validation needed
}
// Service with timestamp-aware operations
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
/// Create a new user with automatic timestamps
Future<TurboResponse> createUser({
required String name,
required int age,
}) async {
final user = User(
id: api.genId,
name: name,
age: age,
);
return createDoc(
doc: user,
doNotifyListeners: true,
);
}
/// Get recently created users
Future<TurboResponse<List<User>>> getRecentUsers() async {
return api.listByQueryWithConverter(
collectionReferenceQuery: (ref) => ref
.orderBy('createdAt', descending: true)
.limit(10),
whereDescription: 'Finding recently created users',
);
}
/// Get recently updated users
Future<TurboResponse<List<User>>> getRecentlyUpdatedUsers() async {
return api.listByQueryWithConverter(
collectionReferenceQuery: (ref) => ref
.orderBy('updatedAt', descending: true)
.limit(10),
whereDescription: 'Finding recently updated users',
);
}
}
// Usage in UI
class RecentUsersView extends StatelessWidget {
final UsersService service;
@override
Widget build(BuildContext context) {
return FutureBuilder<TurboResponse<List<User>>>(
future: service.getRecentUsers(),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return snapshot.data!.fold(
ifSuccess: (users) => ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text('Created: ${user.createdAt?.toString() ?? 'N/A'}'),
trailing: Text('Last updated: ${user.updatedAt?.toString() ?? 'N/A'}'),
);
},
),
orElse: (error) => Text('Error: ${error.message}'),
);
},
);
}
}
Key features of automatic timestamp management:
- Automatic Updates: Timestamps are managed automatically by the API
- Type Safety: Full type conversion between Firestore and Dart
- Query Support: Use timestamps for sorting and filtering
- Audit Trail: Track document creation and modification times
- Zero Configuration: No manual timestamp handling needed
Turbo Firestore API provides comprehensive error handling through TurboResponse
, eliminating the need for try-catch blocks and validation:
class UsersService extends TurboCollectionService<User, UsersApi> {
UsersService({required super.api});
/// Create a new user
Future<TurboResponse> createUser({
required String name,
required int age,
}) async {
final user = User(
id: api.genId,
name: name,
age: age,
);
// The API handles all validation and Firestore exceptions internally
// and returns them wrapped in TurboResponse
return createDoc(
doc: user,
doNotifyListeners: true,
);
}
/// Transfer points between users using a transaction
Future<TurboResponse> transferPoints({
required User fromUser,
required User toUser,
required int points,
}) async {
final updatedFromUser = User(
id: fromUser.id,
documentReference: fromUser.documentReference,
name: fromUser.name,
points: fromUser.points - points,
);
final updatedToUser = User(
id: toUser.id,
documentReference: toUser.documentReference,
name: toUser.name,
points: toUser.points + points,
);
// In transactions, methods automatically throw on failure to abort the transaction
return api.runTransaction((transaction) async {
// Example of throwing inside transaction based on a condition
if (fromUser.points < points) {
TurboResponse.throwException(
error: 'Insufficient points',
message: 'User does not have enough points for transfer',
);
}
await updateDoc(
doc: updatedFromUser,
transaction: transaction,
);
await updateDoc(
doc: updatedToUser,
transaction: transaction,
);
return TurboResponse.emptySuccess();
});
}
}
// Usage in UI with simplified error handling
class CreateUserButton extends StatelessWidget {
final UsersService service;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () async {
final response = await service.createUser(
name: 'John',
age: 25,
);
// Simple fold pattern for handling success/failure
response.fold(
ifSuccess: (_) => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('User created successfully')),
),
orElse: (error) => ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error.message ?? 'Failed to create user')),
),
);
},
child: Text('Create User'),
);
}
}
Key features of TurboResponse error handling:
- No Try-Catch Needed: All Firestore exceptions are automatically caught and wrapped
- Automatic Validation: API handles all validation internally through
TurboWriteable
- Type-safe Responses: Generic type parameter for success value
- Convenient Fold Pattern: Simple success/failure handling with
fold
- Transaction Support: Methods automatically throw to abort transactions on failure
- Manual Throwing: Use
TurboResponse.throwException()
orresponse.tryThrowFail()
for custom conditions - Rich Error Context: Includes error message, stack trace, and location
- Chainable Operations: Combine multiple operations with error short-circuiting
- Automatic Logging: Errors are automatically logged with proper context
-
Document Creation
- Use
api.genId
for new document IDs - Include timestamps using
gNow
- Add user ID for ownership tracking
- Include parent IDs for hierarchical data
- Use
-
Error Handling
- Always wrap operations in try-catch blocks
- Use
TurboResponse
for operation results - Provide user-friendly error messages
- Log errors with proper context
-
State Management
- Check busy state before operations
- Block updates during critical operations
- Handle empty states appropriately
- Clean up resources in dispose
-
User Feedback
- Show confirmation dialogs for destructive actions
- Display loading states during operations
- Provide success/error notifications
- Handle navigation after operations
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the LICENSE file in the root directory of this repository.