Back to Posts

Flutter and Firebase Integration

19 min read

Firebase provides a powerful backend solution for Flutter applications, offering services like authentication, real-time database, cloud storage, and more. This comprehensive guide will walk you through integrating Firebase into your Flutter app and using its key features effectively.

Setting Up Firebase

  1. Create a Firebase Project:

    • Go to the Firebase Console
    • Create a new project
    • Register your app (Android/iOS/Web)
  2. Add Firebase to Flutter:

    # pubspec.yaml
    dependencies:
      flutter:
        sdk: flutter
      firebase_core: ^2.24.2
      firebase_auth: ^4.15.3
      cloud_firestore: ^4.13.6
      firebase_storage: ^11.5.6
      firebase_analytics: ^10.7.4
      firebase_crashlytics: ^3.4.8
  3. Initialize Firebase:

    Future<void> initializeFirebase() async {
      try {
        await Firebase.initializeApp(
          options: DefaultFirebaseOptions.currentPlatform,
        );
        
        // Enable Crashlytics in non-debug mode
        if (kReleaseMode) {
          await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
          FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
        }
        
        // Enable Analytics
        await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(true);
      } catch (e, stack) {
        print('Error initializing Firebase: $e');
        print('Stack trace: $stack');
      }
    }

Authentication

Firebase Authentication with comprehensive error handling:

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  // Stream of auth state changes
  Stream<User?> get authStateChanges => _auth.authStateChanges();

  // Email/Password Sign Up with error handling
  Future<UserCredential?> signUp({
    required String email,
    required String password,
    required String displayName,
  }) async {
    try {
      // Validate input
      if (email.isEmpty || !email.contains('@')) {
        throw FirebaseAuthException(
          code: 'invalid-email',
          message: 'Please provide a valid email address',
        );
      }

      if (password.length < 8) {
        throw FirebaseAuthException(
          code: 'weak-password',
          message: 'Password should be at least 8 characters',
        );
      }

      // Create user
      final userCredential = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );

      // Update display name
      await userCredential.user?.updateDisplayName(displayName);

      return userCredential;
    } on FirebaseAuthException catch (e) {
      String message;
      switch (e.code) {
        case 'email-already-in-use':
          message = 'This email is already registered';
          break;
        case 'invalid-email':
          message = 'Please provide a valid email address';
          break;
        case 'operation-not-allowed':
          message = 'Email/password accounts are not enabled';
          break;
        case 'weak-password':
          message = 'Please provide a stronger password';
          break;
        default:
          message = 'An error occurred during sign up';
      }
      throw FirebaseAuthException(code: e.code, message: message);
    } catch (e) {
      throw Exception('An unexpected error occurred');
    }
  }

  // Email/Password Sign In with rate limiting
  final _signInAttempts = <String, int>{};
  final _signInBlockDuration = const Duration(minutes: 5);
  final _maxSignInAttempts = 3;

  Future<UserCredential> signIn(String email, String password) async {
    try {
      // Check for too many attempts
      if (_signInAttempts[email] != null &&
          _signInAttempts[email]! >= _maxSignInAttempts) {
        throw FirebaseAuthException(
          code: 'too-many-attempts',
          message: 'Too many sign-in attempts. Please try again later.',
        );
      }

      final result = await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );

      // Reset attempts on successful sign in
      _signInAttempts.remove(email);
      return result;
    } on FirebaseAuthException catch (e) {
      // Increment attempts on failure
      _signInAttempts[email] = (_signInAttempts[email] ?? 0) + 1;

      // Schedule attempt reset
      Future.delayed(_signInBlockDuration, () {
        _signInAttempts.remove(email);
      });

      String message;
      switch (e.code) {
        case 'user-not-found':
          message = 'No user found with this email';
          break;
        case 'wrong-password':
          message = 'Invalid password';
          break;
        case 'user-disabled':
          message = 'This account has been disabled';
          break;
        case 'too-many-attempts':
          message = 'Too many sign-in attempts. Please try again later.';
          break;
        default:
          message = 'An error occurred during sign in';
      }
      throw FirebaseAuthException(code: e.code, message: message);
    }
  }

  // Secure password reset
  Future<void> resetPassword(String email) async {
    try {
      await _auth.sendPasswordResetEmail(email: email);
    } on FirebaseAuthException catch (e) {
      String message;
      switch (e.code) {
        case 'invalid-email':
          message = 'Please provide a valid email address';
          break;
        case 'user-not-found':
          message = 'No user found with this email';
          break;
        default:
          message = 'An error occurred during password reset';
      }
      throw FirebaseAuthException(code: e.code, message: message);
    }
  }

  // Secure sign out
  Future<void> signOut() async {
    try {
      await Future.wait([
        _auth.signOut(),
        // Clear any cached data
        FirebaseFirestore.instance.clearPersistence(),
      ]);
    } catch (e) {
      throw Exception('Error signing out: $e');
    }
  }
}

Cloud Firestore

Secure and optimized Firestore operations:

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;
  
  // Enable offline persistence
  Future<void> enablePersistence() async {
    await _firestore.enablePersistence(
      const Settings(persistenceEnabled: true, cacheSizeBytes: 10485760),
    );
  }

  // Secure document creation with validation
  Future<DocumentReference> addUser(Map<String, dynamic> userData) async {
    try {
      // Validate required fields
      if (!userData.containsKey('email') || !userData.containsKey('name')) {
        throw Exception('Missing required fields');
      }

      // Add metadata
      userData['createdAt'] = FieldValue.serverTimestamp();
      userData['updatedAt'] = FieldValue.serverTimestamp();

      // Add document with security rules
      return await _firestore.collection('users').add(userData);
    } catch (e) {
      throw Exception('Error adding user: $e');
    }
  }

  // Optimized batch operations
  Future<void> batchUpdate(List<Map<String, dynamic>> updates) async {
    try {
      final batch = _firestore.batch();
      
      for (var update in updates) {
        final doc = _firestore.collection('users').doc(update['id']);
        update['updatedAt'] = FieldValue.serverTimestamp();
        batch.update(doc, update);
      }

      await batch.commit();
    } catch (e) {
      throw Exception('Error performing batch update: $e');
    }
  }

  // Paginated query with caching
  Stream<List<QueryDocumentSnapshot>> getPaginatedUsers({
    int limit = 20,
    DocumentSnapshot? startAfter,
  }) {
    try {
      var query = _firestore
          .collection('users')
          .orderBy('createdAt', descending: true)
          .limit(limit);

      if (startAfter != null) {
        query = query.startAfterDocument(startAfter);
      }

      return query.snapshots().map((snapshot) => snapshot.docs);
    } catch (e) {
      throw Exception('Error fetching users: $e');
    }
  }

  // Secure document deletion with cascade
  Future<void> deleteUserWithData(String userId) async {
    try {
      final batch = _firestore.batch();
      
      // Delete user document
      final userDoc = _firestore.collection('users').doc(userId);
      batch.delete(userDoc);

      // Delete related data
      final userPosts = await _firestore
          .collection('posts')
          .where('userId', isEqualTo: userId)
          .get();
      
      for (var post in userPosts.docs) {
        batch.delete(post.reference);
      }

      await batch.commit();
    } catch (e) {
      throw Exception('Error deleting user data: $e');
    }
  }
}

Cloud Storage

Secure and optimized file handling:

import 'package:firebase_storage/firebase_storage.dart';
import 'package:image/image.dart' as img;

class StorageService {
  final FirebaseStorage _storage = FirebaseStorage.instance;
  
  // Upload with progress tracking and validation
  Future<String> uploadFile({
    required String path,
    required Uint8List file,
    required String contentType,
    void Function(double)? onProgress,
  }) async {
    try {
      // Validate file size
      if (file.length > 5 * 1024 * 1024) { // 5MB limit
        throw Exception('File size exceeds 5MB limit');
      }

      // Validate content type
      if (!['image/jpeg', 'image/png', 'application/pdf']
          .contains(contentType)) {
        throw Exception('Unsupported file type');
      }

      // Compress image if needed
      Uint8List processedFile = file;
      if (contentType.startsWith('image/')) {
        processedFile = await compressImage(file);
      }

      // Create storage reference
      final ref = _storage.ref().child(path);

      // Upload with metadata
      final metadata = SettableMetadata(
        contentType: contentType,
        customMetadata: {'uploadedAt': DateTime.now().toIso8601String()},
      );

      // Start upload task
      final uploadTask = ref.putData(processedFile, metadata);

      // Track progress
      if (onProgress != null) {
        uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
          final progress = snapshot.bytesTransferred / snapshot.totalBytes;
          onProgress(progress);
        });
      }

      // Wait for completion
      await uploadTask;

      // Return download URL
      return await ref.getDownloadURL();
    } catch (e) {
      throw Exception('Error uploading file: $e');
    }
  }

  // Compress image
  Future<Uint8List> compressImage(Uint8List input) async {
    final image = img.decodeImage(input);
    if (image == null) throw Exception('Invalid image data');

    // Resize if too large
    var processed = image;
    if (image.width > 1920 || image.height > 1080) {
      processed = img.copyResize(
        image,
        width: 1920,
        height: 1080,
        maintainAspectRatio: true,
      );
    }

    // Compress
    return Uint8List.fromList(img.encodeJpg(processed, quality: 85));
  }

  // Secure file deletion
  Future<void> deleteFile(String path) async {
    try {
      final ref = _storage.ref().child(path);
      await ref.delete();
    } catch (e) {
      throw Exception('Error deleting file: $e');
    }
  }
}

Security Best Practices

1. Firestore Security Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Helper functions
    function isAuthenticated() {
      return request.auth != null;
    }
    
    function isOwner(userId) {
      return isAuthenticated() && request.auth.uid == userId;
    }
    
    function isValidUser(data) {
      return data.size() <= 10 && // Limit fields
             data.name is string &&
             data.email is string &&
             data.email.matches('^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$');
    }

    // User collection rules
    match /users/{userId} {
      allow read: if isAuthenticated();
      allow create: if isAuthenticated() && isValidUser(request.resource.data);
      allow update: if isOwner(userId) && isValidUser(request.resource.data);
      allow delete: if isOwner(userId);
    }

    // Posts collection rules
    match /posts/{postId} {
      allow read: if true;
      allow create: if isAuthenticated() && 
                      request.resource.data.userId == request.auth.uid;
      allow update, delete: if isAuthenticated() && 
                             resource.data.userId == request.auth.uid;
    }
  }
}

2. Storage Security Rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    function isAuthenticated() {
      return request.auth != null;
    }
    
    function isValidImage() {
      return request.resource.contentType.matches('image/.*') &&
             request.resource.size <= 5 * 1024 * 1024; // 5MB
    }

    match /users/{userId}/{allPaths=**} {
      allow read: if isAuthenticated();
      allow write: if isAuthenticated() && 
                     request.auth.uid == userId && 
                     isValidImage();
    }
  }
}

Error Handling and Monitoring

class FirebaseErrorHandler {
  static void handleError(dynamic error, StackTrace? stackTrace) {
    if (error is FirebaseException) {
      // Log to Crashlytics
      FirebaseCrashlytics.instance.recordError(
        error,
        stackTrace,
        reason: error.message,
      );

      // Log to Analytics
      FirebaseAnalytics.instance.logEvent(
        name: 'firebase_error',
        parameters: {
          'error_code': error.code,
          'error_message': error.message ?? 'Unknown error',
        },
      );
    }
  }

  static Future<T> runWithErrorHandling<T>(
    Future<T> Function() operation,
  ) async {
    try {
      return await operation();
    } catch (error, stackTrace) {
      handleError(error, stackTrace);
      rethrow;
    }
  }
}

Performance Optimization

1. Query Optimization

// Implement indexing
await FirebaseFirestore.instance
    .collection('users')
    .where('age', isGreaterThan: 18)
    .orderBy('age')
    .limit(20)
    .get();

// Use composite indexes for complex queries
await FirebaseFirestore.instance
    .collection('posts')
    .where('category', isEqualTo: 'tech')
    .where('published', isEqualTo: true)
    .orderBy('createdAt', descending: true)
    .limit(10)
    .get();

2. Caching Strategy

class CacheManager {
  static const cacheDuration = Duration(hours: 1);
  final _cache = <String, CacheEntry>{};

  Future<T> getCachedData<T>(
    String key,
    Future<T> Function() fetchData,
  ) async {
    final now = DateTime.now();
    final entry = _cache[key];

    if (entry != null && now.difference(entry.timestamp) < cacheDuration) {
      return entry.data as T;
    }

    final data = await fetchData();
    _cache[key] = CacheEntry(data: data, timestamp: now);
    return data;
  }
}

class CacheEntry {
  final dynamic data;
  final DateTime timestamp;

  CacheEntry({required this.data, required this.timestamp});
}

Best Practices

  1. Security:

    • Implement proper security rules
    • Validate all user input
    • Use Firebase App Check
    • Implement rate limiting
    • Secure sensitive data
  2. Error Handling:

    • Implement comprehensive error handling
    • Use proper error reporting
    • Provide user-friendly error messages
    • Log errors for debugging
  3. Performance:

    • Optimize queries
    • Implement proper caching
    • Use batch operations
    • Monitor performance metrics
    • Implement pagination
  4. Data Structure:

    • Plan data structure carefully
    • Use proper indexing
    • Implement data validation
    • Follow naming conventions
    • Document data schema

Conclusion

Firebase provides a robust backend solution for Flutter applications. By following these security best practices, implementing proper error handling, and optimizing performance, you can build secure and scalable applications. Remember to:

  • Implement comprehensive security rules
  • Handle errors gracefully
  • Monitor performance
  • Test thoroughly
  • Keep dependencies updated

Next Steps

  1. Implement Firebase Analytics for usage tracking
  2. Set up Firebase Crashlytics for error reporting
  3. Use Firebase Performance Monitoring
  4. Implement Firebase Cloud Functions
  5. Set up Firebase App Distribution

Happy coding with Firebase and Flutter!