Flutter Firebase Entegrasyonu: Auth, Firestore ve Push Bildirim
# Flutter + Firebase Entegrasyonu: Auth, Firestore ve Push Bildirimler
Firebase, doğru kurgulandığında Flutter projeleri için güçlü bir backend hızlandırıcısıdır. Yıllar içinde Firebase ile desteklenen pek çok Flutter uygulaması yayınladım ve her proje bana ölçekte neyin işe yaradığını, neyin baskı altında çöktüğünü öğretti. Bu yazıda Firebase'i bir Flutter projesine entegre etmenin pratik yönlerini ele alıyorum: kimlik doğrulama akışlarından Firestore veri modellemeye, push bildirimlerden hata raporlamaya kadar, ancak sadece production ortamında ortaya çıkan zor kazanılmış derslerle birlikte.
FlutterFire CLI ile Proje Kurulumu
Herhangi bir Firebase kodu yazmadan önce temiz bir proje kurulumu gerekiyor. FlutterFire CLI, google-services.json ile manuel uğraşı tamamen ortadan kaldırır:
class=class="code-string">"code-comment">// FlutterFire CLIclass="code-string">'ı global olarak yükleyin
class=class="code-string">"code-comment">// dart pub global activate flutterfire_cli
class=class="code-string">"code-comment">// Sonra proje kök dizininde:
class=class="code-string">"code-comment">// flutterfire configure --project=firebase-proje-id
class=class="code-string">"code-comment">// main.dart dosyasında
import 'package:firebase_core/firebase_core.dartclass="code-string">';
import 'firebase_options.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}Firebase kullanan uygulamalarımda her zaman geliştirme ve production için ayrı Firebase projeleri tutarım. Ortamları karıştırmak bozuk analizlere, gerçek kullanıcılara ulaşan test push bildirimlerine ve "test için lazımdı" diye gereğinden fazla gevşek tutulan güvenlik kurallarına yol açar.
Firebase Authentication
E-posta ve Şifre ile Kimlik Doğrulama
En yaygın başlangıç noktası. Düzgün hata yönetimi ile tam bir kayıt ve giriş akışı:
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
class=class="code-string">"code-comment">// Kayıt
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-posta doğrulama gönder
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">'Bu e-posta adresi zaten kayıtlı.');
case class="code-string">'weak-password':
throw AuthError(class="code-string">'Şifre en az class="code-number">6 karakter olmalıdır.');
case class="code-string">'invalid-email':
throw AuthError(class="code-string">'Lütfen geçerli bir e-posta adresi girin.');
default:
throw AuthError(class="code-string">'Kayıt başarısız oldu. Lütfen tekrar deneyin.');
}
}
}
class=class="code-string">"code-comment">// Giriş
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">'Geçersiz e-posta veya şifre.');
case class="code-string">'user-disabled':
throw AuthError(class="code-string">'Bu hesap devre dışı bırakılmış.');
default:
throw AuthError(class="code-string">'Giriş başarısız oldu. Lütfen tekrar deneyin.');
}
}
}
class=class="code-string">"code-comment">// Reaktif UI için auth state stream
Stream<User?> get authStateChanges => _auth.authStateChanges();
class=class="code-string">"code-comment">// Çıkış yap
Future<void> signOut() => _auth.signOut();
}Google ile Giriş
Future<UserCredential?> signInWithGoogle() async {
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
if (googleUser == null) return null; class=class="code-string">"code-comment">// Kullanıcı iptal etti
final GoogleSignInAuthentication googleAuth =
await googleUser.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
return await FirebaseAuth.instance.signInWithCredential(credential);
}Kayıt Sonrası Kullanıcı Profili Oluşturma
Firebase kullanan uygulamalarımda, kayıt işleminden hemen sonra mutlaka Firestore'da bir kullanıcı belgesi oluştururum. Sadece FirebaseAuth kullanıcı kayıtlarına güvenmek kısıtlayıcıdır; roller, tercihler veya abonelik durumu gibi özel alanlara er ya da geç ihtiyacınız olacaktır:
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
Veri Modelleme ve CRUD İşlemleri
Firestore bir doküman veritabanıdır, bu yüzden tablolar ve satırlar yerine koleksiyonlar ve dokümanlar olarak düşünmeniz gerekir. Model sınıfları kullanan tip-güvenli bir yaklaşım:
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,
};
}Firestore İçin Repository Kalıbı
class TaskRepository {
final _collection = FirebaseFirestore.instance.collection(class="code-string">'tasks');
class=class="code-string">"code-comment">// Oluştur
Future<String> createTask(Task task) async {
final docRef = await _collection.add(task.toFirestore());
return docRef.id;
}
class=class="code-string">"code-comment">// Oku (gerçek zamanlı 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">// Güncelle
Future<void> updateTask(String taskId, Map<String, dynamic> updates) {
return _collection.doc(taskId).update(updates);
}
class=class="code-string">"code-comment">// Sil
Future<void> deleteTask(String taskId) {
return _collection.doc(taskId).delete();
}
class=class="code-string">"code-comment">// Toplu güncelleme için batch işlemleri
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();
}
}Çevrimdışı Destek
Firestore, mobilde yerleşik çevrimdışı kalıcılığa sahiptir, ancak bunu bilinçli olarak yönetmeniz gerekir:
class=class="code-string">"code-comment">// Önbellek boyutu sınırıyla çevrimdışı kalıcılığı etkinleştir
FirebaseFirestore.instance.settings = const Settings(
persistenceEnabled: true,
cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);
class=class="code-string">"code-comment">// Verinin önbellekten mi sunucudan mı geldiğini kontrol et
stream.listen((snapshot) {
for (final change in snapshot.docChanges) {
if (snapshot.metadata.isFromCache) {
class=class="code-string">"code-comment">// Veri yerel önbellekten geliyor, kullanıcıya gösterge göster
}
}
});Firestore Güvenlik Kuralları
Firebase projelerinin çoğunun başarısız olduğu yer burasıdır. Açık güvenlik kuralları, bir numaralı Firebase güvenlik hatasıdır ve production ortamında `allow read, write: if true;` kurallarıyla her kullanıcının verisini herkese açık hale getiren uygulamalar gördüm.
rules_version = class="code-string">'class="code-number">2';
service cloud.firestore {
match /databases/{database}/documents {
class=class="code-string">"code-comment">// Kullanıcı profilleri: kullanıcılar sadece kendi profillerini okuyup yazabilir
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">// Kullanıcılar kendi rollerini veya oluşturma tarihlerini değiştiremez
}
class=class="code-string">"code-comment">// Görevler: kullanıcılar sadece kendi görevlerine erişebilir
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">// Sadece admin koleksiyonu
match /admin/{document=**} {
allow read, write: if request.auth != null
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == class="code-string">'admin';
}
}
}Firebase kullanan uygulamalarımda, güvenlik kurallarını deploy etmeden önce mutlaka Firebase Emulator Suite ile test ederim. Hatalı bir kural, kullanıcıları sessizce dışarıda bırakabilir veya verileri sessizce ifşa edebilir.
Firebase Cloud Messaging (FCM)
Kurulum ve Token Yönetimi
class PushNotificationService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
Future<void> initialize() async {
class=class="code-string">"code-comment">// İzin iste (iOSclass="code-string">'ta zorunlu, Android class="code-number">13+'da iyi bir uygulama)
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">// Tokenclass="code-string">'ı al
final token = await _messaging.getToken();
if (token != null) {
await _saveTokenToFirestore(token);
}
class=class="code-string">"code-comment">// Token yenilemeyi dinle
_messaging.onTokenRefresh.listen(_saveTokenToFirestore);
}
Future<void> _saveTokenToFirestore(String token) async {
final user = FirebaseAuth.instance.currentUser;
if (user != null) {
await FirebaseFirestore.instance.collection('usersclass="code-string">').doc(user.uid).update({
'fcmTokensclass="code-string">': FieldValue.arrayUnion([token]),
'lastTokenUpdateclass="code-string">': FieldValue.serverTimestamp(),
});
}
}
void _configureForegroundHandler() {
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
class=class="code-string">"code-comment">// Yerel bildirim veya uygulama içi banner göster
if (message.notification != null) {
_showLocalNotification(message);
}
});
}
static Future<void> _configureBackgroundHandler() async {
FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
}
}
class=class="code-string">"code-comment">// Üst düzey bir fonksiyon olmalıdır
@pragma('vm:entry-point')
Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
class=class="code-string">"code-comment">// Arka plan mesajını işle (hafif tut)
}Konu Tabanlı Bildirimler
class=class="code-string">"code-comment">// Hedefli mesajlaşma için kullanıcıları konulara abone et
await FirebaseMessaging.instance.subscribeToTopic(class="code-string">'haberler');
await FirebaseMessaging.instance.subscribeToTopic(class="code-string">'promo_tr');
class=class="code-string">"code-comment">// Abonelikten çık
await FirebaseMessaging.instance.unsubscribeFromTopic(class="code-string">'promo_tr');Crashlytics Entegrasyonu
Crashlytics, production uygulamalar için vazgeçilmezdir. Her projemde kullandığım kurulum:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
class=class="code-string">"code-comment">// Yakalanmamış tüm Flutter hatalarını Crashlyticsclass="code-string">'e ilet
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
class=class="code-string">"code-comment">// Yakalanmamış tüm async hataları Crashlytics'e ilet
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
runApp(const MyApp());
}
class=class="code-string">"code-comment">// Ölümcül olmayan hatalar için özel kayıt
void logError(dynamic error, StackTrace? stack, {String? reason}) {
FirebaseCrashlytics.instance.recordError(
error,
stack,
reason: reason ?? class="code-string">'Ölümcül olmayan hata',
);
}
class=class="code-string">"code-comment">// Hata raporları için kullanıcı tanımlayıcı ayarla
void setUserForCrashReporting(String userId) {
FirebaseCrashlytics.instance.setUserIdentifier(userId);
FirebaseCrashlytics.instance.setCustomKey(class="code-string">'user_role', class="code-string">'premium');
}Sık Yapılan Firebase Hataları
Firebase projelerinde en sık gördüğüm hatalar bunlar, bazılarını kendim de yaptım:
1. Açık Güvenlik Kuralları
Varsayılan test modu kuralları 30 gün sonra sona erer, ancak birçok takım bunları değiştirmeyi unutur. Tüm veritabanınız herkese açık okunabilir ve yazılabilir hale gelir. İlk deploy'dan önce mutlaka düzgün kurallar yazın.
2. Çevrimdışı Durumu Yönetmemek
Firestore işlemleri çevrimdışıyken sessizce kuyruğa alınır. Uygulamanız bunu kullanıcıya bildirmezse, kullanıcı verisinin kaydedildiğini sanabilir oysa hala beklemede olabilir. Her zaman `snapshot.metadata.hasPendingWrites` kontrol edin ve uygun UI gösterin.
3. Sınırsız Sorgular
Bir koleksiyonu `.limit()` olmadan toptan çekmek saatli bir bombadır. 50 dokümanla sorunsuz çalışır, 50.000 ile çöker. Her zaman sayfalama kullanın:
class=class="code-string">"code-comment">// Sayfalanmış sorgu
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. Türetilmiş Veriyi Sadece İstemci Tarafında Tutmak
"Görev sayısı" veya "okunmamış mesaj sayısı" gibi verilere ihtiyacınız varsa, her okumada hesaplamayın. Cloud Functions ile dokümanlar değiştikçe sayaçları güncelleyin. İstemci tarafı toplama ölçeklenmez.
5. FCM Token Yenilemesini Göz Ardı Etmek
FCM tokenları haber vermeden değişir. Token'ı kayıt sırasında bir kez kaydedip bir daha güncellemezseniz, push bildirimler haftalar veya aylar sonra o kullanıcılar için sessizce çalışmayı durduracaktır.
6. Eşzamanlı Yazmalar İçin Transaction Kullanmamak
Birden fazla kullanıcı veya cihaz aynı dokümanı değiştirebiliyorsa, transaction kullanmanız gerekir:
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});
});
}Maliyet Optimizasyonu
Firebase fiyatlandırması, dikkat etmezseniz sizi şaşırtabilir. Uyguladığım stratejiler:
Okuma/Yazma Optimizasyonu
Depolama ve Bant Genişliği
Cloud Functions
İzleme
Firebase kullanan uygulamalarımda, beklenen aylık bütçemin %50, %80 ve %100'ünde fatura uyarıları kurarım. 100K okuma başına 0,06$ ucuz görünür, ta ki uygulamanız viral olana kadar.
Production Kontrol Listesi
Firebase destekli bir Flutter uygulamasını yayınlamadan önce bu listeyi gözden geçirin:
Sonuç
Firebase, Flutter uygulamaları için güçlü bir hızlandırıcıdır, ancak en başından mimari disiplin gerektirir. Kimlik doğrulama, Firestore veri modelleme, push bildirimler ve hata raporlama temel direklerdir ve her birinin yalnızca ölçekte ortaya çıkan ince tuzakları vardır. Bir Firebase prototipi ile bir Firebase production uygulaması arasındaki fark, güvenlik kuralları, çevrimdışı yönetim, maliyet farkındalığı ve düzgün izlemedir.
İsterseniz Firebase mimarinizi production-ready hale getirebiliriz.
İlgili Makaleler
Flutter Performans Optimizasyonu: Kapsamlı Rehber
Flutter uygulamanızın performansını sistematik olarak artırın. Rebuild optimizasyonu, bellek yönetimi, lazy loading ve profiling tekniklerini öğrenin.
Flutter API Integration: REST Servislerle Sağlam Entegrasyon
Flutter'da API entegrasyonunu repository pattern, hata yönetimi ve veri modelleme ile sağlamlaştırın.
Flutter CI/CD: Otomatik Build, Test ve Release Süreci
Flutter projelerinde güvenli ve hızlı teslimat için CI/CD pipeline tasarlayın. Build, test ve release adımlarını otomatikleştirin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç