Flutter Firebase Integration: Auth, Firestore und Push
# 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:
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:
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
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:
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:
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
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:
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.
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
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
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:
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:
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:
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
Speicher und Bandbreite
Cloud Functions
Monitoring
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:
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
Flutter Performance-Optimierung: Vollständiger Leitfaden
Steigern Sie die Performance Ihrer Flutter-App systematisch. Lernen Sie Rebuild-Optimierung, Speichermanagement, Lazy Loading und Profiling.
Flutter API Integration: Robuste REST-Architektur
Stärken Sie Ihre API-Integration in Flutter mit Repository-Pattern, Fehlerbehandlung und sauberem Datenmodell.
Flutter CI/CD: Automatisierte Build-, Test- und Release-Pipeline
Entwerfen Sie eine robuste Flutter-CI/CD-Pipeline mit automatisierten Build-, Test- und Release-Schritten.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen