Flutter Firebase Integration: Auth, Firestore und Push

14 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
Flutter FirebaseFlutter FirestoreFlutter Firebase AuthFlutter push notificationFlutter FCMFlutter CrashlyticsFirebase security rulesFlutter backend

# Flutter Firebase Integration: Auth, Firestore und Push-Benachrichtigungen

Firebase beschleunigt die Produktentwicklung deutlich, wenn Sicherheit und Struktur von Anfang an stimmen. Im Laufe der Jahre habe ich mehrere Flutter-Apps mit Firebase-Backend veröffentlicht, und jedes Projekt hat mir gezeigt, was im großen Maßstab funktioniert und was unter Druck zusammenbricht. Dieser Artikel behandelt die praktische Seite der Firebase-Integration in einem Flutter-Projekt: von Authentifizierungsabläufen über Firestore-Datenmodellierung bis hin zu Push-Benachrichtigungen und Crash-Reporting, zusammen mit Lektionen, die sich nur in der Produktion zeigen.

Projekteinrichtung mit FlutterFire CLI

Bevor Sie Firebase-Code schreiben, brauchen Sie ein sauberes Projekt-Setup. Das FlutterFire CLI macht das manuelle Hantieren mit google-services.json überflüssig:

dart
class=class="code-string">"code-comment">// FlutterFire CLI global installieren
class=class="code-string">"code-comment">// dart pub global activate flutterfire_cli

class=class="code-string">"code-comment">// Dann im Projektstammverzeichnis:
class=class="code-string">"code-comment">// flutterfire configure --project=ihre-firebase-project-id

class=class="code-string">"code-comment">// In main.dart
import class="code-string">'package:firebase_core/firebase_core.dart';
import class="code-string">'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

In meinen Firebase-basierten Apps halte ich immer separate Firebase-Projekte für Entwicklung und Produktion. Das Mischen von Umgebungen führt zu verfälschten Analytics, Test-Push-Benachrichtigungen, die echte Nutzer erreichen, und Sicherheitsregeln, die zu freizügig sind, weil man sie "zum Testen brauchte."

Firebase Authentication

E-Mail- und Passwort-Authentifizierung

Der häufigste Einstiegspunkt. Hier ist ein vollständiger Registrierungs- und Login-Ablauf mit ordentlicher Fehlerbehandlung:

dart
class AuthService {
  final FirebaseAuth _auth = FirebaseAuth.instance;

  class=class="code-string">"code-comment">// Registrierung
  Future<UserCredential?> register(String email, String password) async {
    try {
      final credential = await _auth.createUserWithEmailAndPassword(
        email: email,
        password: password,
      );
      class=class="code-string">"code-comment">// E-Mail-Verifizierung senden
      await credential.user?.sendEmailVerification();
      return credential;
    } on FirebaseAuthException catch (e) {
      switch (e.code) {
        case class="code-string">'email-already-in-use':
          throw AuthError(class="code-string">'Diese E-Mail-Adresse ist bereits registriert.');
        case class="code-string">'weak-password':
          throw AuthError(class="code-string">'Das Passwort muss mindestens class="code-number">6 Zeichen lang sein.');
        case class="code-string">'invalid-email':
          throw AuthError(class="code-string">'Bitte geben Sie eine gültige E-Mail-Adresse ein.');
        default:
          throw AuthError(class="code-string">'Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut.');
      }
    }
  }

  class=class="code-string">"code-comment">// Anmeldung
  Future<UserCredential?> login(String email, String password) async {
    try {
      return await _auth.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
    } on FirebaseAuthException catch (e) {
      switch (e.code) {
        case class="code-string">'user-not-found':
        case class="code-string">'wrong-password':
          throw AuthError(class="code-string">'Ungültige E-Mail oder Passwort.');
        case class="code-string">'user-disabled':
          throw AuthError(class="code-string">'Dieses Konto wurde deaktiviert.');
        default:
          throw AuthError(class="code-string">'Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut.');
      }
    }
  }

  class=class="code-string">"code-comment">// Auth-State-Stream für reaktive UI
  Stream<User?> get authStateChanges => _auth.authStateChanges();

  class=class="code-string">"code-comment">// Abmelden
  Future<void> signOut() => _auth.signOut();
}

Google-Anmeldung

dart
Future<UserCredential?> signInWithGoogle() async {
  final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
  if (googleUser == null) return null; class=class="code-string">"code-comment">// Nutzer hat abgebrochen

  final GoogleSignInAuthentication googleAuth =
      await googleUser.authentication;

  final credential = GoogleAuthProvider.credential(
    accessToken: googleAuth.accessToken,
    idToken: googleAuth.idToken,
  );

  return await FirebaseAuth.instance.signInWithCredential(credential);
}

Nutzerprofil nach der Registrierung anlegen

In meinen Firebase-basierten Apps erstelle ich immer direkt nach der Registrierung ein Firestore-Nutzerdokument. Sich ausschließlich auf FirebaseAuth-Nutzerdaten zu verlassen, ist einschränkend. Man braucht unweigerlich benutzerdefinierte Felder wie Rollen, Einstellungen oder Abonnementstatus:

dart
Future<void> createUserProfile(User user) async {
  final docRef = FirebaseFirestore.instance.collection(class="code-string">'users').doc(user.uid);
  final doc = await docRef.get();

  if (!doc.exists) {
    await docRef.set({
      class="code-string">'email': user.email,
      class="code-string">'displayName': user.displayName ?? class="code-string">'',
      class="code-string">'photoUrl': user.photoURL ?? class="code-string">'',
      class="code-string">'role': class="code-string">'user',
      class="code-string">'createdAt': FieldValue.serverTimestamp(),
      class="code-string">'lastLoginAt': FieldValue.serverTimestamp(),
    });
  } else {
    await docRef.update({
      class="code-string">'lastLoginAt': FieldValue.serverTimestamp(),
    });
  }
}

Cloud Firestore

Datenmodellierung und CRUD-Operationen

Firestore ist eine Dokumentendatenbank, daher müssen Sie in Sammlungen und Dokumenten denken, nicht in Tabellen und Zeilen. Hier ist ein typsicherer Ansatz mit Modellklassen:

dart
class Task {
  final String id;
  final String title;
  final String description;
  final bool isCompleted;
  final DateTime createdAt;
  final String userId;

  Task({
    required this.id,
    required this.title,
    required this.description,
    required this.isCompleted,
    required this.createdAt,
    required this.userId,
  });

  factory Task.fromFirestore(DocumentSnapshot doc) {
    final data = doc.data() as Map<String, dynamic>;
    return Task(
      id: doc.id,
      title: data[class="code-string">'title'] ?? class="code-string">'',
      description: data[class="code-string">'description'] ?? class="code-string">'',
      isCompleted: data[class="code-string">'isCompleted'] ?? false,
      createdAt: (data[class="code-string">'createdAt'] as Timestamp).toDate(),
      userId: data[class="code-string">'userId'] ?? class="code-string">'',
    );
  }

  Map<String, dynamic> toFirestore() => {
    class="code-string">'title': title,
    class="code-string">'description': description,
    class="code-string">'isCompleted': isCompleted,
    class="code-string">'createdAt': Timestamp.fromDate(createdAt),
    class="code-string">'userId': userId,
  };
}

Repository-Pattern für Firestore

dart
class TaskRepository {
  final _collection = FirebaseFirestore.instance.collection(class="code-string">'tasks');

  class=class="code-string">"code-comment">// Erstellen
  Future<String> createTask(Task task) async {
    final docRef = await _collection.add(task.toFirestore());
    return docRef.id;
  }

  class=class="code-string">"code-comment">// Lesen (Echtzeit-Stream)
  Stream<List<Task>> watchUserTasks(String userId) {
    return _collection
        .where(class="code-string">'userId', isEqualTo: userId)
        .orderBy(class="code-string">'createdAt', descending: true)
        .snapshots()
        .map((snapshot) =>
            snapshot.docs.map((doc) => Task.fromFirestore(doc)).toList());
  }

  class=class="code-string">"code-comment">// Aktualisieren
  Future<void> updateTask(String taskId, Map<String, dynamic> updates) {
    return _collection.doc(taskId).update(updates);
  }

  class=class="code-string">"code-comment">// Löschen
  Future<void> deleteTask(String taskId) {
    return _collection.doc(taskId).delete();
  }

  class=class="code-string">"code-comment">// Batch-Operationen für Massenaktualisierungen
  Future<void> completeAllTasks(String userId) async {
    final batch = FirebaseFirestore.instance.batch();
    final tasks = await _collection
        .where(class="code-string">'userId', isEqualTo: userId)
        .where(class="code-string">'isCompleted', isEqualTo: false)
        .get();

    for (final doc in tasks.docs) {
      batch.update(doc.reference, {class="code-string">'isCompleted': true});
    }
    await batch.commit();
  }
}

Offline-Unterstützung

Firestore verfügt auf Mobilgeräten über integrierte Offline-Persistenz, aber man muss bewusst damit umgehen:

dart
class=class="code-string">"code-comment">// Offline-Persistenz mit Cache-Größenlimit aktivieren
FirebaseFirestore.instance.settings = const Settings(
  persistenceEnabled: true,
  cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);

class=class="code-string">"code-comment">// Prüfen ob Daten aus dem Cache oder vom Server kommen
stream.listen((snapshot) {
  for (final change in snapshot.docChanges) {
    if (snapshot.metadata.isFromCache) {
      class=class="code-string">"code-comment">// Daten kommen aus dem lokalen Cache, Indikator anzeigen
    }
  }
});

Firestore-Sicherheitsregeln

Hier scheitern die meisten Firebase-Projekte. Offene Sicherheitsregeln sind der häufigste Firebase-Sicherheitsfehler, und ich habe Produktions-Apps mit `allow read, write: if true;`-Regeln gesehen, die sämtliche Nutzerdaten für jeden zugänglich machen.

javascript
rules_version = class="code-string">'class="code-number">2';
service cloud.firestore {
  match /databases/{database}/documents {

    class=class="code-string">"code-comment">// Nutzerprofile: Nutzer können nur ihr eigenes Profil lesen und schreiben
    match /users/{userId} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null && request.auth.uid == userId;
      allow update: if request.auth != null && request.auth.uid == userId
                    && !request.resource.data.diff(resource.data).affectedKeys()
                        .hasAny([class="code-string">'role', class="code-string">'createdAt']);
      class=class="code-string">"code-comment">// Nutzer können ihre eigene Rolle oder das Erstellungsdatum nicht ändern
    }

    class=class="code-string">"code-comment">// Aufgaben: Nutzer können nur auf ihre eigenen Aufgaben zugreifen
    match /tasks/{taskId} {
      allow read: if request.auth != null
                  && resource.data.userId == request.auth.uid;
      allow create: if request.auth != null
                    && request.resource.data.userId == request.auth.uid
                    && request.resource.data.keys().hasAll([class="code-string">'title', class="code-string">'userId', class="code-string">'createdAt']);
      allow update: if request.auth != null
                    && resource.data.userId == request.auth.uid;
      allow delete: if request.auth != null
                    && resource.data.userId == request.auth.uid;
    }

    class=class="code-string">"code-comment">// Nur-Admin-Sammlung
    match /admin/{document=**} {
      allow read, write: if request.auth != null
                         && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == class="code-string">'admin';
    }
  }
}

In meinen Firebase-basierten Apps schreibe ich immer Sicherheitsregel-Tests mit der Firebase Emulator Suite, bevor ich deploye. Eine fehlerhafte Regel kann Nutzer stillschweigend aussperren oder Daten stillschweigend offenlegen.

Firebase Cloud Messaging (FCM)

Einrichtung und Token-Verwaltung

dart
class PushNotificationService {
  final FirebaseMessaging _messaging = FirebaseMessaging.instance;

  Future<void> initialize() async {
    class=class="code-string">"code-comment">// Berechtigung anfragen (erforderlich auf iOS, gute Praxis ab Android class="code-number">13)
    final settings = await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
      provisional: false,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      await _setupToken();
      _configureForegroundHandler();
      _configureBackgroundHandler();
    }
  }

  Future<void> _setupToken() async {
    class=class="code-string">"code-comment">// Token abrufen
    final token = await _messaging.getToken();
    if (token != null) {
      await _saveTokenToFirestore(token);
    }

    class=class="code-string">"code-comment">// Auf Token-Erneuerung lauschen
    _messaging.onTokenRefresh.listen(_saveTokenToFirestore);
  }

  Future<void> _saveTokenToFirestore(String token) async {
    final user = FirebaseAuth.instance.currentUser;
    if (user != null) {
      await FirebaseFirestore.instance.collection(class="code-string">'users').doc(user.uid).update({
        class="code-string">'fcmTokens': FieldValue.arrayUnion([token]),
        class="code-string">'lastTokenUpdate': FieldValue.serverTimestamp(),
      });
    }
  }

  void _configureForegroundHandler() {
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      class=class="code-string">"code-comment">// Lokale Benachrichtigung oder In-App-Banner anzeigen
      if (message.notification != null) {
        _showLocalNotification(message);
      }
    });
  }

  static Future<void> _configureBackgroundHandler() async {
    FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
  }
}

class=class="code-string">"code-comment">// Muss eine Top-Level-Funktion sein
@pragma(class="code-string">'vm:entry-point')
Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  class=class="code-string">"code-comment">// Hintergrundnachricht verarbeiten (schlank halten)
}

Themenbasierte Benachrichtigungen

dart
class=class="code-string">"code-comment">// Nutzer für gezielte Nachrichten zu Themen abonnieren
await FirebaseMessaging.instance.subscribeToTopic(class="code-string">'neuigkeiten');
await FirebaseMessaging.instance.subscribeToTopic(class="code-string">'promo_de');

class=class="code-string">"code-comment">// Abonnement kündigen
await FirebaseMessaging.instance.unsubscribeFromTopic(class="code-string">'promo_de');

Crashlytics-Integration

Crashlytics ist für Produktions-Apps unverzichtbar. Hier ist das Setup, das ich in jedem Projekt verwende:

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

  class=class="code-string">"code-comment">// Alle nicht abgefangenen Flutter-Fehler an Crashlytics weiterleiten
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;

  class=class="code-string">"code-comment">// Alle nicht abgefangenen asynchronen Fehler an Crashlytics weiterleiten
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };

  runApp(const MyApp());
}

class=class="code-string">"code-comment">// Benutzerdefinierte Protokollierung für nicht-fatale Fehler
void logError(dynamic error, StackTrace? stack, {String? reason}) {
  FirebaseCrashlytics.instance.recordError(
    error,
    stack,
    reason: reason ?? class="code-string">'Nicht-fataler Fehler',
  );
}

class=class="code-string">"code-comment">// Nutzer-ID für Crash-Berichte setzen
void setUserForCrashReporting(String userId) {
  FirebaseCrashlytics.instance.setUserIdentifier(userId);
  FirebaseCrashlytics.instance.setCustomKey(class="code-string">'user_role', class="code-string">'premium');
}

Häufige Firebase-Fehler

Das sind die Fehler, die ich am häufigsten in Firebase-Projekten sehe, einige davon habe ich selbst gemacht:

1. Offene Sicherheitsregeln

Die standardmäßigen Testmodus-Regeln laufen nach 30 Tagen ab, aber viele Teams vergessen, sie zu ersetzen. Die gesamte Datenbank ist dann öffentlich les- und schreibbar. Schreiben Sie immer ordentliche Regeln vor dem ersten Deploy.

2. Keine Offline-Behandlung

Firestore-Operationen werden im Offline-Modus stillschweigend in eine Warteschlange gestellt. Wenn Ihre App das dem Nutzer nicht mitteilt, denkt er möglicherweise, seine Daten seien gespeichert, obwohl sie noch ausstehen. Prüfen Sie immer `snapshot.metadata.hasPendingWrites` und zeigen Sie eine entsprechende UI an.

3. Unbegrenzte Abfragen

Eine gesamte Sammlung ohne `.limit()` abzurufen ist eine tickende Zeitbombe. Mit 50 Dokumenten funktioniert es einwandfrei, mit 50.000 stürzt es ab. Verwenden Sie immer Paginierung:

dart
class=class="code-string">"code-comment">// Paginierte Abfrage
Query<Map<String, dynamic>> paginatedQuery(DocumentSnapshot? lastDoc) {
  var query = _collection.orderBy(class="code-string">'createdAt').limit(class="code-number">20);
  if (lastDoc != null) {
    query = query.startAfterDocument(lastDoc);
  }
  return query;
}

4. Abgeleitete Daten nur clientseitig berechnen

Wenn Sie eine "Aufgabenanzahl" oder "Anzahl ungelesener Nachrichten" benötigen, berechnen Sie sie nicht bei jedem Lesen. Nutzen Sie Cloud Functions, um Zähler zu pflegen, wenn sich Dokumente ändern. Clientseitige Aggregation skaliert nicht.

5. FCM-Token-Erneuerung ignorieren

FCM-Tokens werden ohne Vorwarnung rotiert. Wenn Sie den Token einmal bei der Registrierung speichern und nie aktualisieren, funktionieren Push-Benachrichtigungen nach Wochen oder Monaten stillschweigend nicht mehr für diese Nutzer.

6. Keine Transaktionen bei gleichzeitigen Schreibvorgängen

Wenn mehrere Nutzer oder Geräte dasselbe Dokument ändern können, brauchen Sie Transaktionen:

dart
Future<void> incrementLikeCount(String postId) {
  return FirebaseFirestore.instance.runTransaction((transaction) async {
    final postRef = FirebaseFirestore.instance.collection(class="code-string">'posts').doc(postId);
    final snapshot = await transaction.get(postRef);
    final currentLikes = snapshot.data()?[class="code-string">'likes'] ?? class="code-number">0;
    transaction.update(postRef, {class="code-string">'likes': currentLikes + class="code-number">1});
  });
}

Kostenoptimierung

Die Firebase-Preisgestaltung kann überraschen, wenn man nicht aufpasst. Hier sind die Strategien, die ich befolge:

Lese-/Schreib-Optimierung

  • **Verwenden Sie `.limit()` bei jeder Abfrage.** Firestore berechnet pro Dokumentenlesung, eine unbegrenzte Abfrage auf einer 10K-Dokument-Sammlung kostet also jedes Mal 10K Lesevorgänge.
  • **Cachen Sie aggressiv.** Firestores Offline-Cache bedeutet, dass wiederholte Besuche null Lesevorgänge kosten. Verwenden Sie `GetOptions(source: Source.cache)` für Daten, die sich selten ändern.
  • **Nutzen Sie `select()`, um nur die benötigten Felder abzurufen**, wenn Sie mit großen Dokumenten arbeiten (Firestore berechnet trotzdem einen Lesevorgang, aber die Bandbreite wird reduziert).
  • Speicher und Bandbreite

  • Komprimieren Sie Bilder vor dem Hochladen in Firebase Storage. Ein 5-MB-Foto, das von 1.000 Nutzern hochgeladen wird, verursacht echte Kosten für Speicher und Bandbreite.
  • Richten Sie Lifecycle-Regeln ein, um temporäre Dateien automatisch zu löschen.
  • Cloud Functions

  • Vermeiden Sie verkettete Funktionsaufrufe, bei denen eine Funktion eine andere auslöst. Jeder Aufruf wird einzeln abgerechnet.
  • Verwenden Sie Batch-Schreibvorgänge, anstatt Dokumente in einer Schleife innerhalb einer Cloud Function einzeln zu schreiben.
  • Monitoring

  • Richten Sie **Budgetalarme** in der Firebase-Konsole ein. Ich habe Hobby-Projekte gesehen, die unerwartete Rechnungen angehäuft haben, weil eine Entwicklungsschleife Millionen von Lesevorgängen ausgelöst hat.
  • Nutzen Sie den Firebase-Nutzungsreiter, um herauszufinden, welche Sammlungen die meisten Lesevorgänge verursachen.
  • In meinen Firebase-basierten Apps richte ich immer Abrechnungsalarme bei 50 %, 80 % und 100 % meines erwarteten Monatsbudgets ein. Die 0,06 $ pro 100K Lesevorgänge klingen günstig, bis die App viral geht.

    Produktions-Checkliste

    Bevor Sie eine Firebase-gestützte Flutter-App veröffentlichen, gehen Sie diese Liste durch:

  • [ ] Sicherheitsregeln sind abgesichert und mit dem Firebase Emulator getestet
  • [ ] Separate Firebase-Projekte für Entwicklung und Produktion
  • [ ] Umgebungsspezifische Konfiguration (API-Schlüssel, Projekt-IDs) über Flavors oder Env-Dateien verwaltet
  • [ ] E-Mail-Verifizierungsablauf ist implementiert und erzwungen
  • [ ] FCM-Token-Erneuerung wird behandelt und veraltete Tokens werden bereinigt
  • [ ] Crashlytics ist mit Nutzer-IDs und benutzerdefinierten Schlüsseln konfiguriert
  • [ ] Alle Firestore-Abfragen verwenden `.limit()` oder Paginierung
  • [ ] Offline-Status wird dem Nutzer mit entsprechender UI mitgeteilt
  • [ ] Firestore-Indizes sind für zusammengesetzte Abfragen erstellt (prüfen Sie die Konsole auf fehlende Index-Fehler)
  • [ ] Cloud Functions haben Fehlerbehandlung und Retry-Logik
  • [ ] Budgetalarme sind in der Google Cloud Console konfiguriert
  • [ ] App Check ist aktiviert, um unbefugten API-Zugriff zu verhindern
  • [ ] Datenexport- und Backup-Strategie ist dokumentiert
  • [ ] Datenschutzerklärung deckt die Datenerfassung durch Firebase Analytics und Crashlytics ab
  • Fazit

    Firebase ist ein leistungsstarker Beschleuniger für Flutter-Apps, verlangt aber von Anfang an architektonische Disziplin. Authentifizierung, Firestore-Datenmodellierung, Push-Benachrichtigungen und Crash-Reporting sind die tragenden Säulen, und jede davon hat subtile Fallstricke, die sich erst im großen Maßstab zeigen. Der Unterschied zwischen einem Firebase-Prototyp und einer Firebase-Produktions-App liegt in den Sicherheitsregeln, der Offline-Behandlung, dem Kostenbewusstsein und einer ordentlichen Überwachung.

    Ich unterstütze gern beim Production-Setup Ihrer Firebase-Architektur.

    Verwandte Artikel

    Haben Sie ein Flutter-Projekt?

    Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

    Kontakt aufnehmen