Skip to content

Flutter Firebase Integration — Auth, Firestore, and Cloud Services

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Flutter Firebase Integration. We cover key concepts, practical examples, and best practices to help you master this topic.

Flutter Firebase integration connects mobile apps to Google's cloud platform, providing authentication, real-time database, file storage, push notifications, and analytics services.

What Will You Learn

  • Setting up Firebase in a Flutter project
  • Firebase Authentication with email/password and Google sign-in
  • Cloud Firestore for real-time NoSQL database
  • Firebase Storage for file uploads
  • Firebase Cloud Messaging for push notifications
  • Firebase Analytics and Crashlytics

Why It Matters

Firebase provides a complete backend solution that scales from Prototype to production. Authentication, database, storage, and analytics are the most common backend needs for mobile apps. Firebase handles server infrastructure, leaving you to focus on the app logic. The Firebase Flutter plugins are first-party, well-maintained, and integrate deeply with Flutter's widget system through streams and FutureBuilder patterns.

Real-World Use

The DodaTech Flutter app uses Firebase Authentication for user login (email and Google providers), Cloud Firestore for course data and user progress, Firebase Storage for profile images and course thumbnails, Firebase Cloud Messaging for push notifications about new courses, and Crashlytics for error reporting.

Learning Path

flowchart LR
  A[Local Storage] --> B[Firebase Integration\nYou are here]
  B --> C[Flutter Testing]
  style B fill:#f90,color:#fff

Firebase Setup

Add Firebase to your Flutter project:

# pubspec.yaml
dependencies:
  firebase_core: ^2.25.0
  firebase_auth: ^4.17.0
  cloud_firestore: ^4.15.0
  firebase_storage: ^11.6.0
  firebase_messaging: ^14.7.0
  firebase_analytics: ^10.8.0
  firebase_crashlytics: ^3.4.0

Initialize Firebase in main():

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

Create a Firebase project in the Firebase Console, register your app, and download the google-services.json (Android) or GoogleService-Info.plist (iOS).

Firebase Authentication

Implement email/password authentication:

class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  Stream<User?> get userStream => _auth.authStateChanges();

  User? get currentUser => _auth.currentUser;

  Future<UserCredential> signUp(String email, String password) async {
    try {
      return await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      throw _handleAuthError(e);
    }
  }

  Future<UserCredential> signIn(String email, String password) async {
    try {
      return await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      throw _handleAuthError(e);
    }
  }

  Future<void> signOut() async {
    await _auth.signOut();
  }

  Future<void> sendPasswordReset(String email) async {
    await _auth.sendPasswordResetEmail(email: email);
  }

  String _handleAuthError(FirebaseAuthException e) {
    switch (e.code) {
      case 'weak-password': return 'Password is too weak';
      case 'email-already-in-use': return 'Account already exists';
      case 'user-not-found': return 'No account found with this email';
      case 'wrong-password': return 'Incorrect password';
      case 'invalid-credential': return 'Invalid email or password';
      default: return 'Authentication failed: ${e.message}';
    }
  }
}

// Usage in a login screen:
class LoginScreen extends StatefulWidget {
  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final _authService = AuthService();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  String? _error;

  Future<void> _login() async {
    try {
      await _authService.signIn(
        _emailController.text.trim(),
        _passwordController.text,
      );
    } on Exception catch (e) {
      setState(() => _error = e.toString());
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login')),
      body: Padding(
        padding: EdgeInsets.all(24),
        child: Column(
          children: [
            TextField(controller: _emailController, decoration: InputDecoration(labelText: 'Email')),
            TextField(controller: _passwordController, decoration: InputDecoration(labelText: 'Password'), obscureText: true),
            if (_error != null) Text(_error!, style: TextStyle(color: Colors.red)),
            ElevatedButton(onPressed: _login, child: Text('Login')),
          ],
        ),
      ),
    );
  }
}

Use authStateChanges() to listen to authentication state. The stream emits the current user or null when signed out. Wrap the app with a StreamBuilder that switches between login and home screens.

Cloud Firestore

Firestore is a NoSQL document database with real-time sync:

class FirestoreService {
  final FirebaseFirestore _db = FirebaseFirestore.instance;

  // Create
  Future<void> addCourse(Map<String, dynamic> course) async {
    await _db.collection('courses').add(course);
  }

  // Read (single document)
  Future<Map<String, dynamic>?> getCourse(String id) async {
    final doc = await _db.collection('courses').doc(id).get();
    return doc.data();
  }

  // Read (realtime stream)
  Stream<List<Map<String, dynamic>>> getCoursesStream() {
    return _db
        .collection('courses')
        .orderBy('createdAt', descending: true)
        .snapshots()
        .map((snapshot) => snapshot.docs
            .map((doc) => {'id': doc.id, ...doc.data()})
            .toList());
  }

  // Update
  Future<void> updateCourse(String id, Map<String, dynamic> data) async {
    await _db.collection('courses').doc(id).update(data);
  }

  // Delete
  Future<void> deleteCourse(String id) async {
    await _db.collection('courses').doc(id).delete();
  }

  // Query with filters
  Stream<List<Map<String, dynamic>>> getCoursesByCategory(String category) {
    return _db
        .collection('courses')
        .where('category', isEqualTo: category)
        .snapshots()
        .map((snapshot) => snapshot.docs
            .map((doc) => {'id': doc.id, ...doc.data()})
            .toList());
  }

  // Batch write
  Future<void> batchWrite() async {
    final batch = _db.batch();
    final ref1 = _db.collection('courses').doc();
    final ref2 = _db.collection('courses').doc();
    batch.set(ref1, {'title': 'Course A'});
    batch.set(ref2, {'title': 'Course B'});
    await batch.commit();
  }
}

Firestore queries are indexed. Create Composite indexes in the Firebase Console for queries with multiple where clauses and orderBy.

Using Firestore in a Widget

StreamBuilder listens to Firestore streams reactively:

class CourseListScreen extends StatelessWidget {
  final _firestoreService = FirestoreService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Courses')),
      body: StreamBuilder<List<Map<String, dynamic>>>(
        stream: _firestoreService.getCoursesStream(),
        builder: (context, snapshot) {
          if (snapshot.hasError) {
            return Center(
              child: Text('Error: ${snapshot.error}'),
            );
          }

          if (!snapshot.hasData) {
            return Center(child: CircularProgressIndicator());
          }

          final courses = snapshot.data!;
          if (courses.isEmpty) {
            return Center(child: Text('No courses yet'));
          }

          return ListView.builder(
            itemCount: courses.length,
            itemBuilder: (_, i) {
              final course = courses[i];
              return ListTile(
                leading: CircleAvatar(child: Text('${i + 1}')),
                title: Text(course['title'] as String),
                subtitle: Text(course['category'] as String? ?? ''),
                trailing: Icon(Icons.chevron_right),
              );
            },
          );
        },
      ),
    );
  }
}

The StreamBuilder rebuilds whenever Firestore data changes. The stream is automatically cancelled when the widget is disposed.

Firebase Storage

Upload and download files:

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

class StorageService {
  final FirebaseStorage _storage = FirebaseStorage.instance;

  Future<String> uploadProfileImage(String userId, File imageFile) async {
    try {
      final ref = _storage.ref().child('profiles/$userId.jpg');
      final uploadTask = ref.putFile(imageFile);

      // Track progress
      uploadTask.snapshotEvents.listen((snapshot) {
        final progress = snapshot.bytesTransferred / snapshot.totalBytes * 100;
        print('Upload: ${progress.toStringAsFixed(1)}%');
      });

      final snapshot = await uploadTask;
      final downloadUrl = await snapshot.ref.getDownloadURL();
      return downloadUrl;
    } on FirebaseException catch (e) {
      throw Exception('Upload failed: ${e.message}');
    }
  }

  Future<String> uploadCourseImage(String courseId, File imageFile) async {
    final ref = _storage.ref().child('courses/$courseId/thumbnail.jpg');
    await ref.putFile(imageFile);
    return await ref.getDownloadURL();
  }

  Future<void> deleteImage(String path) async {
    final ref = _storage.ref().child(path);
    await ref.delete();
  }

  Future<List<String>> listFiles(String prefix) async {
    final ref = _storage.ref().child(prefix);
    final result = await ref.listAll();
    return result.items.map((item) => item.name).toList();
  }
}

Storage paths organize files hierarchically. Use metadata for custom properties. Set security rules in Firebase Console to control read/write access.

Firebase Cloud Messaging (FCM)

Send and receive push notifications:

import 'package:firebase_messaging/firebase_messaging.dart';

class NotificationService {
  final FirebaseMessaging _messaging = FirebaseMessaging.instance;

  Future<void> initialize() async {
    // Request permission
    NotificationSettings settings = await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('Push notifications authorized');
    }

    // Get FCM token
    final token = await _messaging.getToken();
    print('FCM Token: $token');

    // Listen to foreground messages
    FirebaseMessaging.onMessage.listen(_handleForegroundMessage);

    // Listen to background message taps
    FirebaseMessaging.onMessageOpenedApp.listen(_handleMessageOpened);

    // Handle notification that opened the app from terminated state
    final initialMessage = await _messaging.getInitialMessage();
    if (initialMessage != null) {
      _handleMessageOpened(initialMessage);
    }
  }

  void _handleForegroundMessage(RemoteMessage message) {
    final notification = message.notification;
    if (notification != null) {
      print('Foreground notification: ${notification.title}');
      // Show in-app notification
    }
  }

  void _handleMessageOpened(RemoteMessage message) {
    final data = message.data;
    print('Notification tapped: $data');
    // Navigate to specific screen based on data
  }
}

FCM requires Firebase project setup on both Android and iOS. Test notifications from the Firebase Console before implementing custom server logic.

Firebase Analytics and Crashlytics

Track user behavior and monitor crashes:

import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observers.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';

class AnalyticsService {
  final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
  final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;

  Future<void> initialize() async {
    // Enable crashlytics collection
    await _crashlytics.setCrashlyticsCollectionEnabled(true);

    // Set user identifier
    await _crashlytics.setUserIdentifier('user_123');
  }

  Future<void> logScreenView(String screenName) async {
    await _analytics.logScreenView(
      screenName: screenName,
      screenClass: screenName,
    );
  }

  Future<void> logEvent(String name, {Map<String, dynamic>? parameters}) async {
    await _analytics.logEvent(
      name: name,
      parameters: parameters,
    );
  }

  Future<void> logError(Object error, StackTrace stackTrace) async {
    await _crashlytics.recordError(error, stackTrace);
  }

  Future<void> setUserProperty(String name, String value) async {
    await _analytics.setUserProperty(name: name, value: value);
  }

  // FirebaseAnalyticsObserver for automatic screen tracking
  FirebaseAnalyticsObserver get analyticsObserver =>
      FirebaseAnalyticsObserver(analytics: _analytics);
}

Crashlytics reports errors automatically. Add FirebaseAnalyticsObserver to your NavigatorObservers for automatic screen tracking.

Common Mistakes

  1. Not handling FirebaseAuth exceptions properly: FirebaseAuthException has specific error codes. Handle each case (weak password, email in use, user not found) with user-friendly messages.

  2. Reading Firestore documents without error handling: Network failures cause exceptions. Use try-catch or handle the error state in StreamBuilder.

  3. Not setting Firestore security rules: By default, Firestore is open in test mode. Set proper rules before production to prevent unauthorized access.

  4. Downloading files without checking cache: Firebase Storage downloads use bandwidth. Cache downloaded files locally to reduce costs and improve offline access.

  5. Not configuring Deep Links for notification navigation: When a user taps a notification, navigate to the correct screen using the data payload. Store navigation routes in notification data.

Practice Questions

  1. How does authStateChanges() help manage authentication UI state?
  2. What is the difference between Firestore's get() and snapshots()?
  3. How do Firebase Storage security rules differ from Firestore rules?
  4. What is the purpose of the FCM token?
  5. Challenge: Build a chat app with Firebase. Use Firebase Auth for authentication, Cloud Firestore for messages (with real-time listener), Firebase Storage for image sharing, and FCM for push notifications. Implement read receipts and typing indicators.

Mini Project

Build a social media feed app:

  • Firebase Auth for user registration and login
  • Cloud Firestore for posts (image URL, caption, likes, comments)
  • Firebase Storage for image uploads
  • Real-time feed with StreamBuilder
  • Like and comment functionality
  • Push notifications for new likes
  • Analytics events for post creation and interaction

FAQ

How much does Firebase cost?

Firebase has a generous free tier (Spark plan). Authentication, Firestore (1GB storage, 50K reads/day), Storage (5GB), and Analytics are free. Usage beyond limits requires the Blaze pay-as-you-go plan.

Can I use Firebase without Google Play Services?

Some Firebase services require Google Play Services (Android). For devices without them, use Firebase Authentication with custom tokens or alternative backends.

How do I handle Firestore offline data?

Firestore supports offline persistence by default. Documents read while online are cached. Writes are queued locally and synced when connectivity returns.

What is the difference between Realtime Database and Firestore?

Firestore is the newer, more scalable database with better querying, stronger security rules, and automatic multi-region replication. Realtime Database is simpler and lower latency for small data sets.

How do I migrate from Firebase test mode to production?

Set Firestore security rules to allow only authenticated users. Restrict Storage access. Enable App Check to prevent abuse. Move to Blaze plan for production scale.

What is Next

Now that you understand Firebase integration, learn about testing in Flutter. Proceed to Flutter Testing for unit tests, widget tests, and integration tests. Then explore Flutter Performance for optimization and profiling.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro