Back to blog
April 4, 2024
6 min read

Firebase Cloud Firestore in Flutter

Real-time database solution

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

  1. Setup Dependencies

    # pubspec.yaml
    dependencies:
      firebase_core: ^2.24.2
      cloud_firestore: ^4.13.6
    
  2. Configure Firebase

    Android (android/app/src/main/AndroidManifest.xml):

    <manifest>
      <uses-permission android:name="android.permission.INTERNET"/>
    </manifest>
    
  3. 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;
        }
      }
    }
    
  4. 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),
        };
      }
    }
    
  5. 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

  1. 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();
      }
    }
    
  2. 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),
            });
          }
        });
      }
    }
    
  3. 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

  1. Data Structure

    • Design efficient collections
    • Use subcollections when appropriate
    • Implement proper indexing
    • Optimize queries
  2. Security

    • Set up security rules
    • Validate data
    • Handle sensitive information
    • Implement proper access control
  3. Performance

    • Use pagination
    • Implement caching
    • Optimize queries
    • Handle offline data

Common Use Cases

  1. User Management

    • User profiles
    • User settings
    • User preferences
    • User relationships
  2. Content Management

    • Posts and articles
    • Comments and replies
    • Media content
    • Categories and tags
  3. 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.