fire_crud 3.1.13
fire_crud: ^3.1.13 copied to clipboard
CRUD Operations for firestore
Fire CRUD #
A comprehensive Dart package for performing typed, hierarchical CRUD operations on Firestore. Fire CRUD simplifies working with Firestore by providing a model-based approach that handles serialization, nested document structures, efficient querying, real-time streaming, and pagination. It eliminates boilerplate for mapping Firestore data to Dart models, supports parent-child relationships for complex data hierarchies, and integrates seamlessly with fire_api for Firestore interactions.
Why Fire CRUD? #
Firestore's native API is powerful but verbose for typed applications, especially with nested collections. Fire CRUD addresses this by:
- Enabling type-safe models with automatic path generation for hierarchical data.
- Providing utilities for CRUD on individual documents and collections.
- Supporting efficient collection views with caching, streaming windows, and smart pagination to handle large datasets without full loads.
- Offering atomic operations and existence checks for robust data management.
- Reducing repetitive code for serialization/deserialization using artifact functions.
Ideal for Flutter/Dart apps needing scalable, real-time data persistence.
Installation #
Add to your pubspec.yaml
:
dependencies:
fire_crud: ^latest_version
fire_api: ^latest_version # Required for Firestore access
dev_dependencies:
build_runner: ^latest_version # If using code generation (optional)
Run:
flutter pub get
Setup Requirements #
Fire CRUD relies on fire_api for Firestore connectivity. Follow its setup guide to initialize Firebase in your app (e.g., via Firebase.initializeApp()
in main()
).
Key Components #
1. FireModel #
Represents a model type for Firestore documents or subcollections. Handles:
- Serialization (
toMap
,fromMap
) and construction. - Path templating for nested hierarchies (e.g.,
parent/{parent.id}/child/{child.id}
). - Exclusive documents (fixed ID) vs. dynamic collections.
Use for root and child models.
2. ModelCrud (Mixin) #
Core mixin for your data classes. Provides:
documentPath
: Full Firestore path.childModels
: List ofFireModel
for nested children.- CRUD methods:
get<T>
,set<T>
,add<T>
,delete<T>
,stream<T>
. - Collection ops:
walk<T>
,view<T>
,paginate<T>
,streamAll<T>
,getAll<T>
,count<T>
. - Atomic updates:
setAtomic<T>
,updateAtomic<T>
. - Parent navigation:
parentModel<T>()
,parentModelPath()
.
Implements ModelAccessor
for type-safe access.
3. FireCrud (Singleton) #
Global entry point ($crud
). Manages:
- Model registration (
registerModel
,registerModels
). - Root-level CRUD:
$get<T>
,$set<T>
, etc. - Artifact setup for global serialization:
setupArtifact(fromMap, toMap, construct)
. - Path resolution:
modelForPath
,getCrudForDocumentPath
.
Access via import 'package:fire_crud/fire_crud.dart';
and use $crud
.
4. ModelAccessor (Abstract Interface) #
Defines access patterns for models and children. Implemented by ModelCrud
and FireCrud
.
5. CollectionViewer #
Efficient viewer for collections with:
- Indexed access (
getAt(index)
). - Streaming windows with padding for smooth navigation.
- Caching (up to
memorySize
snapshots). - Size tracking and auto-retargeting.
- Cleanup for memory management.
Use for large lists with partial loading.
6. ModelUtility #
Static helpers for:
- Model selection (
selectChildModel<T>
). - Pagination (
pullPage<T>
returnsModelPage<T>
). - Diffing updates (
getUpdates(before, after)
for minimal Firestore ops). - Flattening/unflattening nested maps.
7. ModelPage #
Paginated result with items
, nextPage()
, and cursors. Supports forward/reverse.
Defining Models #
Models must mix in ModelCrud
and define childModels
for nesting.
import 'package:fire_crud/fire_crud.dart';
// Example: User with settings (unique) and notes (collection)
class User with ModelCrud {
final String name;
final int age;
// Constructor, copyWith, toMap, fromMap (use packages like freezed or json_serializable)
const User({required this.name, required this.age});
@override
List<FireModel> get childModels => [
// Unique sub-document: users/{userId}/data/settings
FireModel.artifact<Usersettings>(
'data',
exclusiveDocumentId: 'settings',
),
// Collection: users/{userId}/notes/{noteId}
FireModel.artifact<Note>('notes'),
];
}
class Usersettings with ModelCrud {
final bool darkMode;
const Usersettings({this.darkMode = false});
@override
List<FireModel> get childModels => []; // Leaf model
}
class Note with ModelCrud {
final String title;
final String content;
const Note({required this.title, required this.content});
@override
List<FireModel> get childModels => []; // Leaf model
}
- Use
FireModel.artifact
for registered serialization (see Setup). - Paths auto-generate: Root
users/{id}
, childusers/{id}/notes/{noteId}
.
Setup #
-
Initialize Firestore (via fire_api):
import 'package:fire_api/fire_api.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(MyApp()); }
-
Register Artifact Functions (global serialization):
// In main() or init $crud.setupArtifact( (map) => YourArtifact.fromMap(map), // Deserialize (obj) => obj.toMap(), // Serialize () => YourArtifact(), // Construct empty );
-
Register Root Models:
$crud.registerModels([ FireModel.artifact<User>('users'), // Add other roots ]);
- Call once at app startup.
- Enables type-safe access via
$crud.$model<User>(id)
.
Usage #
Basic CRUD on Root Models #
// Create/Add
User newUser = const User(name: 'Alice', age: 30);
User addedUser = await $crud.$add(newUser); // Auto-ID
// Or with ID
await $crud.$set('user123', newUser);
// Read
User? user = await $crud.$get<User>('user123');
// Update
await $crud.$update<User>('user123', {'age': 31});
// Atomic Update
await $crud.$updateAtomic<User>('user123', (initial) {
return initial?.copyWith(age: (initial.age ?? 0) + 1) ?? User(name: '', age: 1);
});
// Delete
await $crud.$delete<User>('user123');
// Stream
Stream<User?> userStream = $crud.$stream<User>('user123');
Unique Models (No ID) #
For single-instance docs (e.g., settings):
// Ensure exists (create if absent)
Usersettings settings = await $crud.ensureExistsUnique<Usersettings>(
const Usersettings(darkMode: true),
);
// Read/Update
Usersettings? current = await $crud.getUnique<Usersettings>();
await $crud.updateUnique<Usersettings>({'darkMode': false});
Nested Models (Children) #
Access via parent:
// From root
User user = $crud.$model<User>('user123');
Usersettings settings = user.modelUnique<Usersettings>(); // Unique child
// Add to collection child
Note newNote = const Note(title: 'Hello', content: 'World');
Note addedNote = await user.$add<Note>(newNote);
// Read child
Note? note = await user.$get<Note>('note456');
// Update child
await user.$update<Note>('note456', {'content': 'Updated'});
// Stream child
Stream<Note?> noteStream = user.$stream<Note>('note456');
// Parent navigation
Note note = ...;
User parentUser = note.parentModel<User>();
Collections #
Pagination
// Paginate notes (50 per page)
ModelPage<Note>? page = await user.paginate<Note>(
pageSize: 20,
query: (ref) => ref.orderBy('title'),
reversed: false,
);
List<Note> notes = page?.items ?? [];
ModelPage<Note>? next = await page?.nextPage();
Streaming All
Stream<List<Note>> notesStream = user.streamAll<Note>(
query: (ref) => ref.where('title', isNotEqualTo: '').orderBy('timestamp'),
);
Walker (Traversal)
CollectionWalker<Note> walker = user.walk<Note>(
query: (ref) => ref.orderBy('title'),
);
Note? first = await walker.next(); // Or previous() for reverse
Viewer (Indexed Access)
CollectionViewer<Note> viewer = user.view<Note>();
Note? noteAtIndex5 = await viewer.getAt(5); // Efficient fetch with caching
viewer.stream.listen((updatedViewer) {
// Handle updates
});
int totalSize = viewer.size;
Advanced Features #
Existence Checks
bool exists = await user.$exists<Note>('note456');
bool uniqueExists = await user.existsUnique<Usersettings>();
Ensure Exists
// Create if absent
Note ensured = await user.$ensureExists<Note>('note456', defaultNote);
Change Notification (Diff-Based Update)
Note before = ...;
Note after = before.copyWith(content: 'New');
await user.$change('note456', before, after); // Minimal update
Delete All
await user.deleteAll<Note>(query: (ref) => ref.where('deleted', isEqualTo: true));
Examples #
Full App Flow #
// main.dart
void main() async {
// Firebase init...
$crud.setupArtifact(/* your functions */);
$crud.registerModels([FireModel.artifact<User>('users')]);
runApp(MyApp());
}
// In a service
class UserService {
Future<User> createUser(String name, int age) async {
User user = User(name: name, age: age);
return await $crud.$add(user);
}
Future<List<Note>> getUserNotes(String userId) async {
User user = $crud.$model<User>(userId);
return await user.getAll<Note>(query: (ref) => ref.orderBy('timestamp', descending: true));
}
Stream<List<Note>> streamNotes(String userId) {
User user = $crud.$model<User>(userId);
return user.streamAll<Note>();
}
}
Pagination in UI #
class NotesList extends StatefulWidget {
final String userId;
NotesList(this.userId);
@override
_NotesListState createState() => _NotesListState();
}
class _NotesListState extends State<NotesList> {
ModelPage<Note>? _currentPage;
List<Note> _notes = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadNotes();
}
Future<void> _loadNotes() async {
User user = $crud.$model<User>(widget.userId);
_currentPage = await user.paginate<Note>(pageSize: 20);
setState(() {
_notes = _currentPage?.items ?? [];
_loading = false;
});
}
Future<void> _loadMore() async {
if (_currentPage != null) {
ModelPage<Note>? next = await _currentPage!.nextPage();
if (next != null) {
setState(() {
_notes.addAll(next.items);
_currentPage = next;
});
}
}
}
@override
Widget build(BuildContext context) {
if (_loading) return CircularProgressIndicator();
return ListView.builder(
itemCount: _notes.length + (_currentPage?.hasMore ?? false ? 1 : 0),
itemBuilder: (context, index) {
if (index == _notes.length) {
_loadMore(); // Load on demand
return CircularProgressIndicator();
}
return ListTile(title: Text(_notes[index].title));
},
);
}
}
Best Practices #
- Serialization: Use consistent
toMap
/fromMap
(e.g., viajson_annotation
orfreezed
). - Performance: Use
CollectionViewer
for large lists; limit queries withwhere
. - Hierarchy: Define
childModels
only for direct subcollections to avoid deep nesting issues. - Error Handling: Wrap ops in try-catch for
FirestoreException
. - Testing: Mock Firestore with
fire_api
's testing utils.
Limitations #
- Requires explicit model registration.
- Nested paths assume even segment counts (collection/doc pairs).
- No built-in validation; add via model constructors.
For issues or contributions, see GitHub.