Firebase Cloud Firestore in Flutter
Firebase Cloud Firestore is a flexible, scalable NoSQL cloud database that provides real-time data synchronization and offline support. This guide will show you how to implement Firestore in your Flutter applications.
Why Use Firebase Cloud Firestore?
Firebase Cloud Firestore offers several advantages:
- Real-time synchronization
- Offline support
- Scalable solution
- Flexible data structure
- Powerful queries
- Automatic scaling
- Cost-effective
Implementation Steps
-
Setup Dependencies
# pubspec.yaml dependencies: firebase_core: ^2.24.2 cloud_firestore: ^4.13.6
-
Configure Firebase
Android (android/app/src/main/AndroidManifest.xml):
<manifest> <uses-permission android:name="android.permission.INTERNET"/> </manifest>
-
Create Firestore Service
class FirestoreService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; // Collection References CollectionReference get usersCollection => _firestore.collection('users'); CollectionReference get postsCollection => _firestore.collection('posts'); CollectionReference get commentsCollection => _firestore.collection('comments'); // Create Document Future<DocumentReference> createDocument({ required String collection, required Map<String, dynamic> data, }) async { try { return await _firestore.collection(collection).add(data); } catch (e) { print('Error creating document: $e'); rethrow; } } // Read Document Future<DocumentSnapshot> getDocument({ required String collection, required String documentId, }) async { try { return await _firestore.collection(collection).doc(documentId).get(); } catch (e) { print('Error getting document: $e'); rethrow; } } // Update Document Future<void> updateDocument({ required String collection, required String documentId, required Map<String, dynamic> data, }) async { try { await _firestore .collection(collection) .doc(documentId) .update(data); } catch (e) { print('Error updating document: $e'); rethrow; } } // Delete Document Future<void> deleteDocument({ required String collection, required String documentId, }) async { try { await _firestore.collection(collection).doc(documentId).delete(); } catch (e) { print('Error deleting document: $e'); rethrow; } } // Query Documents Future<QuerySnapshot> queryDocuments({ required String collection, required List<QueryConstraint> constraints, }) async { try { Query query = _firestore.collection(collection); for (var constraint in constraints) { query = query.where( constraint.field, isEqualTo: constraint.value, ); } return await query.get(); } catch (e) { print('Error querying documents: $e'); rethrow; } } // Stream Documents Stream<QuerySnapshot> streamDocuments({ required String collection, required List<QueryConstraint> constraints, }) { try { Query query = _firestore.collection(collection); for (var constraint in constraints) { query = query.where( constraint.field, isEqualTo: constraint.value, ); } return query.snapshots(); } catch (e) { print('Error streaming documents: $e'); rethrow; } } }
-
Create Data Models
class User { final String id; final String name; final String email; final String photoUrl; final DateTime createdAt; User({ required this.id, required this.name, required this.email, required this.photoUrl, required this.createdAt, }); factory User.fromFirestore(DocumentSnapshot doc) { final data = doc.data() as Map<String, dynamic>; return User( id: doc.id, name: data['name'] ?? '', email: data['email'] ?? '', photoUrl: data['photoUrl'] ?? '', createdAt: (data['createdAt'] as Timestamp).toDate(), ); } Map<String, dynamic> toMap() { return { 'name': name, 'email': email, 'photoUrl': photoUrl, 'createdAt': Timestamp.fromDate(createdAt), }; } } class Post { final String id; final String userId; final String title; final String content; final List<String> images; final DateTime createdAt; final DateTime updatedAt; Post({ required this.id, required this.userId, required this.title, required this.content, required this.images, required this.createdAt, required this.updatedAt, }); factory Post.fromFirestore(DocumentSnapshot doc) { final data = doc.data() as Map<String, dynamic>; return Post( id: doc.id, userId: data['userId'] ?? '', title: data['title'] ?? '', content: data['content'] ?? '', images: List<String>.from(data['images'] ?? []), createdAt: (data['createdAt'] as Timestamp).toDate(), updatedAt: (data['updatedAt'] as Timestamp).toDate(), ); } Map<String, dynamic> toMap() { return { 'userId': userId, 'title': title, 'content': content, 'images': images, 'createdAt': Timestamp.fromDate(createdAt), 'updatedAt': Timestamp.fromDate(updatedAt), }; } }
-
Create Repository
class UserRepository { final FirestoreService _firestoreService = FirestoreService(); Future<User> createUser(User user) async { final docRef = await _firestoreService.createDocument( collection: 'users', data: user.toMap(), ); return User( id: docRef.id, name: user.name, email: user.email, photoUrl: user.photoUrl, createdAt: user.createdAt, ); } Future<User?> getUser(String userId) async { final doc = await _firestoreService.getDocument( collection: 'users', documentId: userId, ); return doc.exists ? User.fromFirestore(doc) : null; } Future<void> updateUser(User user) async { await _firestoreService.updateDocument( collection: 'users', documentId: user.id, data: user.toMap(), ); } Future<void> deleteUser(String userId) async { await _firestoreService.deleteDocument( collection: 'users', documentId: userId, ); } Stream<List<User>> streamUsers() { return _firestoreService .streamDocuments(collection: 'users', constraints: []) .map((snapshot) => snapshot.docs .map((doc) => User.fromFirestore(doc)) .toList()); } } class PostRepository { final FirestoreService _firestoreService = FirestoreService(); Future<Post> createPost(Post post) async { final docRef = await _firestoreService.createDocument( collection: 'posts', data: post.toMap(), ); return Post( id: docRef.id, userId: post.userId, title: post.title, content: post.content, images: post.images, createdAt: post.createdAt, updatedAt: post.updatedAt, ); } Future<Post?> getPost(String postId) async { final doc = await _firestoreService.getDocument( collection: 'posts', documentId: postId, ); return doc.exists ? Post.fromFirestore(doc) : null; } Future<void> updatePost(Post post) async { await _firestoreService.updateDocument( collection: 'posts', documentId: post.id, data: post.toMap(), ); } Future<void> deletePost(String postId) async { await _firestoreService.deleteDocument( collection: 'posts', documentId: postId, ); } Stream<List<Post>> streamUserPosts(String userId) { return _firestoreService .streamDocuments( collection: 'posts', constraints: [ QueryConstraint(field: 'userId', value: userId), ], ) .map((snapshot) => snapshot.docs .map((doc) => Post.fromFirestore(doc)) .toList()); } }
Advanced Features
-
Batch Operations
class BatchService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; Future<void> batchWrite(List<BatchOperation> operations) async { final batch = _firestore.batch(); for (var operation in operations) { switch (operation.type) { case BatchOperationType.create: batch.set( _firestore.collection(operation.collection).doc(), operation.data, ); break; case BatchOperationType.update: batch.update( _firestore.collection(operation.collection).doc(operation.id), operation.data, ); break; case BatchOperationType.delete: batch.delete( _firestore.collection(operation.collection).doc(operation.id), ); break; } } await batch.commit(); } }
-
Transaction Operations
class TransactionService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; Future<void> runTransaction( Future<void> Function(Transaction) updateFunction, ) async { await _firestore.runTransaction(updateFunction); } Future<void> incrementCounter(String documentId) async { await _firestore.runTransaction((transaction) async { final docRef = _firestore.collection('counters').doc(documentId); final doc = await transaction.get(docRef); if (!doc.exists) { transaction.set(docRef, {'count': 1}); } else { transaction.update(docRef, { 'count': FieldValue.increment(1), }); } }); } }
-
Complex Queries
class QueryService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; Future<QuerySnapshot> getPostsByDateRange( DateTime startDate, DateTime endDate, ) async { return await _firestore .collection('posts') .where('createdAt', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate)) .where('createdAt', isLessThanOrEqualTo: Timestamp.fromDate(endDate)) .orderBy('createdAt', descending: true) .get(); } Future<QuerySnapshot> searchPosts(String query) async { return await _firestore .collection('posts') .where('title', isGreaterThanOrEqualTo: query) .where('title', isLessThanOrEqualTo: query + '\uf8ff') .get(); } }
Best Practices
-
Data Structure
- Design efficient collections
- Use subcollections when appropriate
- Implement proper indexing
- Optimize queries
-
Security
- Set up security rules
- Validate data
- Handle sensitive information
- Implement proper access control
-
Performance
- Use pagination
- Implement caching
- Optimize queries
- Handle offline data
Common Use Cases
-
User Management
- User profiles
- User settings
- User preferences
- User relationships
-
Content Management
- Posts and articles
- Comments and replies
- Media content
- Categories and tags
-
Real-time Features
- Live updates
- Chat applications
- Notifications
- Activity feeds
Conclusion
Implementing Firebase Cloud Firestore in your Flutter application provides a powerful and flexible database solution. By following these guidelines and implementing the provided examples, you can create a robust data management system that supports real-time updates and offline functionality.