Lesson 27 of 77 intermediate

Firebase Essentials: Auth, FCM, Crashlytics, Firestore

Production Firebase Integration Every Flutter Dev Must Know

Open interactive version (quiz + challenge)

Real-world analogy

Firebase is like hiring a team of specialists for your app: a security guard (Auth) who checks IDs at the door, a mail carrier (FCM) who delivers push notifications even when the app is closed, a black-box recorder (Crashlytics) that captures everything right before a crash, and a live spreadsheet (Firestore) that updates everyone's screen the moment a value changes.

What is it?

Firebase provides a suite of production services for Flutter apps: Firebase Auth for identity, FCM for push notifications, Crashlytics for crash reporting, Firestore for real-time cloud database, and Remote Config for feature flags. Understanding the correct integration patterns and security model is essential for production apps.

Real-world relevance

In a school management platform: Firebase Auth handles teacher/admin login with Google Sign-In and custom backend JWT via ID tokens. Crashlytics captures production crashes with userId and screenName breadcrumbs. FCM sends instant notifications when a student submits an assignment. Firestore powers the real-time attendance board with live document listeners. Remote Config enables/disables the beta AI feedback feature per school without an app store update.

Key points

Code example

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_remote_config/firebase_remote_config.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // Crashlytics — catch all Flutter and Dart errors
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  // FCM background handler must be top-level
  FirebaseMessaging.onBackgroundMessage(_fcmBackgroundHandler);
  runApp(const App());
}

@pragma('vm:entry-point')
Future<void> _fcmBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  await _processNotificationData(message.data);
}

// Auth with GoRouter
class AuthNotifier extends ChangeNotifier {
  final FirebaseAuth _auth;
  User? _currentUser;
  StreamSubscription<User?>? _sub;

  AuthNotifier(this._auth) {
    _sub = _auth.authStateChanges().listen((user) {
      _currentUser = user;
      notifyListeners();
    });
  }

  bool get isAuthenticated => _currentUser != null;
  Future<String?> getIdToken() => _currentUser?.getIdToken();
  Future<void> signOut() => _auth.signOut();

  @override
  void dispose() { _sub?.cancel(); super.dispose(); }
}

// FCM Token Management
class NotificationService {
  final FirebaseMessaging _messaging = FirebaseMessaging.instance;
  final UserApi _api;
  NotificationService(this._api);

  Future<void> initialize() async {
    final settings = await _messaging.requestPermission(alert: true, badge: true, sound: true);
    if (settings.authorizationStatus != AuthorizationStatus.authorized) return;

    final token = await _messaging.getToken();
    if (token != null) await _api.registerFcmToken(token);

    _messaging.onTokenRefresh.listen((newToken) => _api.registerFcmToken(newToken));
    FirebaseMessaging.onMessage.listen(_handleForeground);
    FirebaseMessaging.onMessageOpenedApp.listen(_handleTap);
    final initial = await _messaging.getInitialMessage();
    if (initial != null) _handleTap(initial);
  }

  void _handleForeground(RemoteMessage message) =>
      debugPrint('Foreground: ${message.notification?.title}');

  void _handleTap(RemoteMessage message) {
    final screen = message.data['screen'] as String?;
    if (screen != null) router.push(screen);
  }
}

// Firestore Real-Time Query
class AttendanceRepository {
  final FirebaseFirestore _db = FirebaseFirestore.instance;

  Stream<List<AttendanceRecord>> watchAttendance({
    required String classId, required DateTime date,
  }) {
    return _db.collection('classes').doc(classId)
        .collection('attendance')
        .where('date', isEqualTo: Timestamp.fromDate(date))
        .orderBy('studentName')
        .snapshots()
        .map((snap) => snap.docs.map((d) => AttendanceRecord.fromDoc(d)).toList());
  }

  Future<void> markAttendance({
    required String classId, required String studentId, required bool present,
  }) async {
    await _db.collection('classes').doc(classId)
        .collection('attendance').doc(studentId)
        .set({
          'studentId': studentId, 'present': present,
          'markedAt': FieldValue.serverTimestamp(),
          'markedBy': FirebaseAuth.instance.currentUser?.uid,
        }, SetOptions(merge: true));
  }
}

// Remote Config Feature Flags
class FeatureFlagService {
  final FirebaseRemoteConfig _rc = FirebaseRemoteConfig.instance;

  Future<void> initialize() async {
    await _rc.setConfigSettings(RemoteConfigSettings(
      fetchTimeout: const Duration(seconds: 10),
      minimumFetchInterval: const Duration(hours: 1),
    ));
    await _rc.setDefaults({'enable_ai_feedback': false, 'max_class_size': 40});
    await _rc.fetchAndActivate();
  }

  bool get isAiFeedbackEnabled => _rc.getBool('enable_ai_feedback');
}

Line-by-line walkthrough

  1. 1. Firebase.initializeApp must complete before any Firebase service is used
  2. 2. FlutterError.onError catches Flutter widget tree errors and framework exceptions
  3. 3. PlatformDispatcher.instance.onError catches errors in async code outside the Flutter framework
  4. 4. FCM background handler must be top-level — it runs in a separate isolate with no access to class state
  5. 5. AuthNotifier wraps authStateChanges() in a ChangeNotifier — GoRouter uses this for reactive routing
  6. 6. getIdToken() returns a cached JWT, refreshing automatically when expired
  7. 7. requestPermission on iOS shows the system dialog — check the result before registering the token
  8. 8. onTokenRefresh fires when FCM assigns a new token — always update your backend
  9. 9. where plus orderBy requires a composite Firestore index — create it in the Firebase Console
  10. 10. FieldValue.serverTimestamp() uses the Firestore server time — avoids clock skew issues
  11. 11. minimumFetchInterval of 1 hour prevents excessive Remote Config fetches

Spot the bug

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const App());
}

class NotificationSetup extends StatefulWidget { }
class _State extends State<NotificationSetup> {
  @override
  void initState() {
    super.initState();
    FirebaseMessaging.onBackgroundMessage((message) async {
      print('Background: ${message.notification?.title}');
    });
  }
}
Need a hint?
Two issues: missing crash handler setup and an invalid background message handler location.
Show answer
Bug 1: No Crashlytics setup — FlutterError.onError and PlatformDispatcher.instance.onError are not set, meaning crash reports are lost. Add both in main() before runApp(). Bug 2: FirebaseMessaging.onBackgroundMessage() must be called with a top-level function (not a lambda or method), and it must be set before runApp() — not inside a StatefulWidget's initState(). The background handler runs in a separate isolate where class instances are not accessible. Move to main() with a top-level @pragma('vm:entry-point') function.

Explain like I'm 5

Firebase is like a set of superpowers for your app. Auth is a bouncer who remembers faces — once someone logs in, they stay logged in. Crashlytics is a spy camera that takes a photo right before the app crashes. FCM is a postal system that delivers a letter to someone's phone even when they are not home. Firestore is a whiteboard that everyone in the room can see update in real time.

Fun fact

FCM delivers over 400 billion messages per day across Android, iOS, and web. It is the backbone of push notifications for most of the world's apps. When you send a push notification from your app, it travels through Google's infrastructure, which maintains persistent connections to every Android device on Earth — this is why push notifications arrive even when the app is completely closed.

Hands-on challenge

Set up complete Firebase integration for a school management app: (1) initialize Firebase with Crashlytics error handling in main(), (2) create an AuthNotifier ChangeNotifier that exposes the auth state stream, (3) set user context in Crashlytics on login, (4) register and refresh the FCM token, (5) create a Firestore stream query for real-time class announcements, (6) add a Remote Config feature flag for a new parent portal feature.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery