Flutter Firebase Entegrasyonu: Auth, Firestore ve Push Bildirim

14 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
Flutter FirebaseFlutter FirestoreFlutter Firebase AuthFlutter push notificationFlutter FCMFlutter CrashlyticsFirebase security rulesFlutter backend

# 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:

dart
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ışı:

dart
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ş

dart
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:

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

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:

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,
  };
}

Firestore İçin Repository Kalıbı

dart
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:

dart
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.

javascript
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

dart
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

dart
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:

dart
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:

dart
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:

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});
  });
}

Maliyet Optimizasyonu

Firebase fiyatlandırması, dikkat etmezseniz sizi şaşırtabilir. Uyguladığım stratejiler:

Okuma/Yazma Optimizasyonu

  • **Her sorguda `.limit()` kullanın.** Firestore doküman okuma başına ücret alır, yani 10K dokümanlık bir koleksiyona sınırsız sorgu her seferinde 10K okumaya mal olur.
  • **Agresif önbellek kullanın.** Firestore'un çevrimdışı önbelleği, tekrar ziyaretlerin sıfır okumaya mal olması anlamına gelir. Sık değişmeyen veriler için `GetOptions(source: Source.cache)` kullanın.
  • **Büyük dokümanlarla çalışırken sadece ihtiyacınız olan alanları çekmek için `select()` kullanın** (Firestore yine de bir okuma sayar, ancak bant genişliğini azaltır).
  • Depolama ve Bant Genişliği

  • Resimleri Firebase Storage'a yüklemeden önce sıkıştırın. 1.000 kullanıcının yüklediği 5MB'lık fotoğraf, depolama ve bant genişliğinde gerçek paraya mal olur.
  • Geçici dosyaları otomatik silmek için yaşam döngüsü kuralları belirleyin.
  • Cloud Functions

  • Bir fonksiyonun diğerini tetiklediği zincirleme fonksiyon çağrılarından kaçının. Her çağrı ayrı faturalandırılır.
  • Cloud Function içinde bir döngüde tek tek doküman yazmak yerine toplu yazma (batch) kullanın.
  • İzleme

  • Firebase konsolunda **bütçe uyarıları** ayarlayın. Bir geliştirme döngüsü milyonlarca okumayı tetiklediği için beklenmedik faturalar alan hobi projeleri gördüm.
  • Hangi koleksiyonların en fazla okuma ürettiğini belirlemek için Firebase Kullanım sekmesini kullanın.
  • 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:

  • [ ] Güvenlik kuralları kilitli ve Firebase Emulator ile test edilmiş
  • [ ] Geliştirme ve production için ayrı Firebase projeleri
  • [ ] Ortama özel konfigürasyon (API anahtarları, proje ID'leri) flavor veya env dosyaları ile yönetiliyor
  • [ ] E-posta doğrulama akışı uygulanmış ve zorunlu kılınmış
  • [ ] FCM token yenileme yönetiliyor ve eski tokenlar temizleniyor
  • [ ] Crashlytics, kullanıcı tanımlayıcıları ve özel anahtarlarla yapılandırılmış
  • [ ] Tüm Firestore sorguları `.limit()` veya sayfalama kullanıyor
  • [ ] Çevrimdışı durum kullanıcıya uygun UI ile bildiriliyor
  • [ ] Bileşik sorgular için Firestore indeksleri oluşturulmuş (konsolda eksik indeks hatalarını kontrol edin)
  • [ ] Cloud Functions hata yönetimi ve yeniden deneme mantığına sahip
  • [ ] Google Cloud Console'da bütçe uyarıları yapılandırılmış
  • [ ] Yetkisiz API erişimini önlemek için App Check etkinleştirilmiş
  • [ ] Veri dışa aktarma ve yedekleme stratejisi belgelenmiş
  • [ ] Gizlilik politikası Firebase Analytics ve Crashlytics veri toplama konusunu kapsıyor
  • 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 Projeniz mi Var?

    iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

    İletişime Geç