CodeSphere Logo
Backend2024-01-0515 min read

Firebase Integration with Flutter: Complete Guide

By Hasnain Makada

Firebase Integration with Flutter: Complete Guide

Firebase provides a comprehensive backend-as-a-service platform that integrates seamlessly with Flutter. This guide covers everything you need to know about integrating Firebase services into your Flutter applications.

1. Setting Up Firebase

Install Firebase CLI

npm install -g firebase-tools
firebase login

Initialize Firebase in Your Project

# In your Flutter project root
firebase init

# Install FlutterFire CLI
dart pub global activate flutterfire_cli

# Configure Firebase for Flutter
flutterfire configure

Add Firebase Dependencies

dependencies:
  firebase_core: ^2.24.2
  firebase_auth: ^4.15.3
  cloud_firestore: ^4.13.6
  firebase_storage: ^11.5.6
  firebase_messaging: ^14.7.10
  firebase_analytics: ^10.7.4

2. Firebase Authentication

Basic Setup

import 'package:firebase_auth/firebase_auth.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  
  // Get current user
  User? get currentUser => _auth.currentUser;
  
  // Auth state changes stream
  Stream<User?> get authStateChanges => _auth.authStateChanges();
}

Email/Password Authentication

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  
  // Sign up with email and password
  Future<UserCredential?> signUpWithEmailAndPassword(
    String email,
    String password,
  ) async {
    try {
      UserCredential result = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      return result;
    } on FirebaseAuthException catch (e) {
      throw _handleAuthException(e);
    }
  }
  
  // Sign in with email and password
  Future<UserCredential?> signInWithEmailAndPassword(
    String email,
    String password,
  ) async {
    try {
      UserCredential result = await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      return result;
    } on FirebaseAuthException catch (e) {
      throw _handleAuthException(e);
    }
  }
  
  // Sign out
  Future<void> signOut() async {
    await _auth.signOut();
  }
  
  // Handle authentication exceptions
  String _handleAuthException(FirebaseAuthException e) {
    switch (e.code) {
      case 'weak-password':
        return 'The password provided is too weak.';
      case 'email-already-in-use':
        return 'The account already exists for that email.';
      case 'user-not-found':
        return 'No user found for that email.';
      case 'wrong-password':
        return 'Wrong password provided for that user.';
      default:
        return 'An error occurred. Please try again.';
    }
  }
}

Google Sign-In Integration

import 'package:google_sign_in/google_sign_in.dart';

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  final GoogleSignIn _googleSignIn = GoogleSignIn();
  
  Future<UserCredential?> signInWithGoogle() async {
    try {
      // Trigger the authentication flow
      final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
      
      if (googleUser == null) return null;
      
      // Obtain the auth details from the request
      final GoogleSignInAuthentication googleAuth = 
          await googleUser.authentication;
      
      // Create a new credential
      final credential = GoogleAuthProvider.credential(
        accessToken: googleAuth.accessToken,
        idToken: googleAuth.idToken,
      );
      
      // Sign in to Firebase with the Google credential
      return await _auth.signInWithCredential(credential);
    } catch (e) {
      throw 'Google sign-in failed: $e';
    }
  }
}

3. Cloud Firestore Database

Basic CRUD Operations

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
  final FirebaseFirestore _db = FirebaseFirestore.instance;
  
  // Create document
  Future<void> createUser(String uid, Map<String, dynamic> userData) async {
    try {
      await _db.collection('users').doc(uid).set(userData);
    } catch (e) {
      throw 'Failed to create user: $e';
    }
  }
  
  // Read document
  Future<DocumentSnapshot> getUser(String uid) async {
    try {
      return await _db.collection('users').doc(uid).get();
    } catch (e) {
      throw 'Failed to get user: $e';
    }
  }
  
  // Update document
  Future<void> updateUser(String uid, Map<String, dynamic> data) async {
    try {
      await _db.collection('users').doc(uid).update(data);
    } catch (e) {
      throw 'Failed to update user: $e';
    }
  }
  
  // Delete document
  Future<void> deleteUser(String uid) async {
    try {
      await _db.collection('users').doc(uid).delete();
    } catch (e) {
      throw 'Failed to delete user: $e';
    }
  }
  
  // Stream of documents
  Stream<QuerySnapshot> getUsersStream() {
    return _db.collection('users').snapshots();
  }
}

Advanced Queries

class FirestoreService {
  final FirebaseFirestore _db = FirebaseFirestore.instance;
  
  // Complex query with multiple conditions
  Future<List<QueryDocumentSnapshot>> getFilteredUsers({
    required String city,
    required int minAge,
    int limit = 10,
  }) async {
    try {
      QuerySnapshot querySnapshot = await _db
          .collection('users')
          .where('city', isEqualTo: city)
          .where('age', isGreaterThanOrEqualTo: minAge)
          .orderBy('age')
          .limit(limit)
          .get();
      
      return querySnapshot.docs;
    } catch (e) {
      throw 'Failed to get filtered users: $e';
    }
  }
  
  // Pagination
  Future<List<QueryDocumentSnapshot>> getUsersPaginated({
    DocumentSnapshot? lastDocument,
    int limit = 10,
  }) async {
    try {
      Query query = _db
          .collection('users')
          .orderBy('createdAt', descending: true)
          .limit(limit);
      
      if (lastDocument != null) {
        query = query.startAfterDocument(lastDocument);
      }
      
      QuerySnapshot querySnapshot = await query.get();
      return querySnapshot.docs;
    } catch (e) {
      throw 'Failed to get paginated users: $e';
    }
  }
}

4. Firebase Storage

File Upload and Download

import 'package:firebase_storage/firebase_storage.dart';
import 'dart:io';

class StorageService {
  final FirebaseStorage _storage = FirebaseStorage.instance;
  
  // Upload file
  Future<String> uploadFile(File file, String path) async {
    try {
      Reference ref = _storage.ref().child(path);
      UploadTask uploadTask = ref.putFile(file);
      
      // Monitor upload progress
      uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
        double progress = 
            (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
        print('Upload progress: ${progress.toStringAsFixed(2)}%');
      });
      
      TaskSnapshot snapshot = await uploadTask;
      String downloadUrl = await snapshot.ref.getDownloadURL();
      return downloadUrl;
    } catch (e) {
      throw 'Failed to upload file: $e';
    }
  }
  
  // Upload with metadata
  Future<String> uploadFileWithMetadata(
    File file,
    String path,
    Map<String, String> metadata,
  ) async {
    try {
      Reference ref = _storage.ref().child(path);
      
      SettableMetadata settableMetadata = SettableMetadata(
        customMetadata: metadata,
        contentType: 'image/jpeg',
      );
      
      UploadTask uploadTask = ref.putFile(file, settableMetadata);
      TaskSnapshot snapshot = await uploadTask;
      
      return await snapshot.ref.getDownloadURL();
    } catch (e) {
      throw 'Failed to upload file with metadata: $e';
    }
  }
  
  // Delete file
  Future<void> deleteFile(String path) async {
    try {
      Reference ref = _storage.ref().child(path);
      await ref.delete();
    } catch (e) {
      throw 'Failed to delete file: $e';
    }
  }
}

5. Push Notifications with FCM

Setup and Configuration

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

class NotificationService {
  final FirebaseMessaging _messaging = FirebaseMessaging.instance;
  final FlutterLocalNotificationsPlugin _localNotifications =
      FlutterLocalNotificationsPlugin();
  
  Future<void> initialize() async {
    // Request permission
    NotificationSettings settings = await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );
    
    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
      
      // Get FCM token
      String? token = await _messaging.getToken();
      print('FCM Token: $token');
      
      // Initialize local notifications
      await _initializeLocalNotifications();
      
      // Handle background messages
      FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
      
      // Handle foreground messages
      FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
      
      // Handle notification taps
      FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageOpenedApp);
    }
  }
  
  Future<void> _initializeLocalNotifications() async {
    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');
    
    const DarwinInitializationSettings initializationSettingsIOS =
        DarwinInitializationSettings();
    
    const InitializationSettings initializationSettings =
        InitializationSettings(
      android: initializationSettingsAndroid,
      iOS: initializationSettingsIOS,
    );
    
    await _localNotifications.initialize(
      initializationSettings,
      onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
    );
  }
  
  void _handleForegroundMessage(RemoteMessage message) {
    print('Handling a foreground message: ${message.messageId}');
    _showLocalNotification(message);
  }
  
  void _handleMessageOpenedApp(RemoteMessage message) {
    print('Message clicked!');
    // Navigate to specific screen
  }
  
  Future<void> _showLocalNotification(RemoteMessage message) async {
    const AndroidNotificationDetails androidPlatformChannelSpecifics =
        AndroidNotificationDetails(
      'high_importance_channel',
      'High Importance Notifications',
      channelDescription: 'This channel is used for important notifications.',
      importance: Importance.high,
      priority: Priority.high,
    );
    
    const NotificationDetails platformChannelSpecifics =
        NotificationDetails(android: androidPlatformChannelSpecifics);
    
    await _localNotifications.show(
      message.hashCode,
      message.notification?.title,
      message.notification?.body,
      platformChannelSpecifics,
    );
  }
  
  void _onDidReceiveNotificationResponse(
      NotificationResponse notificationResponse) {
    // Handle notification tap
  }
}

// Top-level function for background message handling
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Handling a background message: ${message.messageId}');
}

6. Firebase Analytics

Basic Analytics Implementation

import 'package:firebase_analytics/firebase_analytics.dart';

class AnalyticsService {
  final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
  
  // Set user properties
  Future<void> setUserProperties({
    required String userId,
    required String userType,
  }) async {
    await _analytics.setUserId(id: userId);
    await _analytics.setUserProperty(name: 'user_type', value: userType);
  }
  
  // Log custom events
  Future<void> logCustomEvent(String eventName, Map<String, Object> parameters) async {
    await _analytics.logEvent(name: eventName, parameters: parameters);
  }
  
  // Log screen views
  Future<void> logScreenView(String screenName) async {
    await _analytics.logScreenView(screenName: screenName);
  }
  
  // Log purchases
  Future<void> logPurchase({
    required String itemId,
    required String itemName,
    required double value,
    required String currency,
  }) async {
    await _analytics.logPurchase(
      currency: currency,
      value: value,
      parameters: {
        'item_id': itemId,
        'item_name': itemName,
      },
    );
  }
}

7. Security Rules

Firestore Security Rules Example

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Users can only access their own documents
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    
    // Public posts that anyone can read, but only authenticated users can write
    match /posts/{postId} {
      allow read: if true;
      allow write: if request.auth != null;
    }
    
    // Private posts that only the owner can access
    match /private_posts/{postId} {
      allow read, write: if request.auth != null && 
        request.auth.uid == resource.data.ownerId;
    }
  }
}

Firebase Storage Security Rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    // Users can only upload to their own folder
    match /users/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    
    // Public images that anyone can read
    match /public/{allPaths=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
  }
}

Conclusion

Firebase provides a powerful, scalable backend solution for Flutter applications. This guide covered the essential services:

  1. Authentication - Secure user management
  2. Firestore - Real-time NoSQL database
  3. Storage - File storage and management
  4. Cloud Messaging - Push notifications
  5. Analytics - User behavior tracking
  6. Security Rules - Data protection

Start with the services that match your immediate needs and expand as your application grows. Remember to implement proper error handling, security rules, and monitor your usage to optimize costs and performance.

Tags:

FlutterFirebaseBackendAuthentication