Sitemap
Kadriye Macit

Kadriye Macit

Sr. Flutter Developer - Software Engineer || Google Developer Expert for Flutter&Dart and Firebase

Bir Moda Uygulamasında Firebase (Part 3): Bildirimler, Gözlem ve Dersler

Android’de hiç gelmeyen bildirimler, Crashlytics ile çökmeden önceki son adımlar ve canlıdan çıkan 20 hata, 20 çözüm

7 min readOct 26, 2026

--

Press enter or click to view image in full size

Bu yazı, “Bir Moda Uygulamasında Firebase” serisinin son bölümü. Part 1’de temelleri, Part 2’de Firebase Auth’u ve Firestore ile DM özelliğini anlattım. Bu bölümde bildirimlere, gözlemlenebilirliğe ve canlıdan çıkan derslere bakıyoruz.

8. Firebase Cloud Messaging: bildirimin cihaza ulaşması kolay, görünmesi zor

Neden? Takip istekleri, yorumlar ve DM’ler için bildirim gerekiyordu. FCM, Android ve iOS’a (APNs üzerinden) tek bir API ile bildirim göndermemizi sağlıyor. Bildirimleri backend gönderiyor; uygulamanın görevi token’ı backend’e iletmek ve gelen mesajı doğru göstermek.

Nasıl? Token yönetimi. Bir kullanıcının birden fazla cihazı olabilir. Bu yüzden token’ı kullanıcıya değil, (kullanıcı, cihaz ID'si, platform) üçlüsüne bağlıyoruz. Token giriş anında login isteğiyle birlikte gidiyor ve onTokenRefresh ile her yenilendiğinde tekrar gönderiliyor. Logout'ta backend o cihazın token'ını silmeli; aksi halde çıkış yapmış bir cihaz başkasının bildirimlerini almaya devam eder.

Nasıl? Üç durum, üç farklı davranış. FCM’de bir mesajın nasıl görüneceği, uygulamanın durumuna ve mesajın tipine bağlı:

Press enter or click to view image in full size

Arka plan handler’ı bu tabloya göre yazılmış:

@pragma('vm:entry-point') // release build'de tree-shaking'e karşı
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
WidgetsFlutterBinding.ensureInitialized();
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(); // ayrı bir isolate'tayız
}
// notification payload'u varsa OS zaten gösterdi; çift bildirim olmasın
if (message.notification == null) {
await presentAndroidNotification(message);
}
}

Bu handler top-level bir fonksiyon olmalı, çünkü ayrı bir isolate’ta çalışır ve uygulamanızın belleğine erişemez. FirebaseMessaging.onBackgroundMessage bir kez ve runApp'ten önce kaydedilmeli. Android sadece ilk kayıtlı handler'ı tutar.

Bildirim başlığını okurken de iki kaynağı birden kontrol ediyoruz. Backend bazen notification, bazen sadece datagönderebiliyor:

String? notificationTitleOf(RemoteMessage message) {
final title = message.notification?.title;
if (title != null && title.isNotEmpty) return title;
return _stringData(message, const ['title', 'notification_title']);
}

Hata-çözüm: Android’de hiç bildirim gelmiyordu. Bu, projenin en öğretici hatasıydı; çünkü tek bir neden değil, üç ayrı neden aynı belirtiyi veriyordu:

Press enter or click to view image in full size

1. İzin, Activity olmadan istendi. Android 13+ POST_NOTIFICATIONS iznini çalışma zamanında ister. Biz bunu runApp'ten önce, AppStart içinde istiyorduk. O an bir Activity olmadığı için istek hata fırlattı. Hata try/catch içinde olduğu için, aynı bloğun geri kalanı (token alma, onMessage dinleyicisi) da sessizce atlandı. Çözüm: izni ilk frame'den sonra istemek.

WidgetsBinding.instance.addPostFrameCallback((_) {
PushNotificationService.instance.requestPermissions();
});

2. Yanlış bildirim ikonu. Manifest’te varsayılan bildirim ikonu olarak adaptive launcher ikonu (@mipmap/ic_launcher) verilmişti. Android bu durumda bildirimi göstermeyebilir ya da beyaz bir kare gösterir. Bildirim ikonu şeffaf arka planlı, tek renkli bir drawable olmalı (@drawable/ic_stat_notification).

3. Yanlış Firebase uygulaması. Dart tarafındaki firebase_options.dart dosyasındaki Android appId, prod paket adına kayıtlı Firebase Android uygulaması değil, başka bir uygulamayı gösteriyordu. Token üretiliyor, backend gönderiyor, ama mesaj cihaza hiç ulaşmıyordu. Part 1'deki "tek doğruluk kaynağı" uyarısının nedeni bu.

Bu düzeltmeyle birlikte manifest’teki elle yazılmış FlutterFirebaseMessagingService tanımlarını da kaldırdık; bu servisleri firebase_messaging eklentisi zaten kendi manifest'iyle ekliyor.

iOS tarafı için iki not:

  • iOS’ta getToken(), APNs token'ı gelmeden çağrılırsa apns-token-not-set hatası verir. Uygulama açılır açılmaz token istemek yerine getAPNSToken()'ın dolmasını bekleyin ya da onTokenRefresh'e güvenin.
  • setForegroundNotificationPresentationOptions() parametresiz çağrıldığında tüm seçenekler false olur. Yani uygulama açıkken iOS bildirim göstermez. Bu bizim bilinçli tercihimiz (DM ekranı açıkken bildirim istemiyoruz), ama çoğu ekip bunu fark etmeden yapıyor.

Test etmek için: Firebase konsolundaki “Test mesajı gönder” ekranı sadece notification payload'u gönderir. Data-only senaryoları test etmek için FCM HTTP v1 API'sini doğrudan kullanın. Uygulamayı ayrı ayrı ön planda, arka planda ve tamamen kapalıyken deneyin.

9. Analytics: olayları tek bir kapıdan geçirmek

Neden? Bir moda uygulamasında asıl soru “kullanıcılar hangi kombinlere bakıyor ve hangi ürün linkine tıklıyor?” sorusu. Firebase Analytics ücretsiz, BigQuery’ye dışa aktarılabiliyor ve Remote Config ile A/B Testing’in hedefleme altyapısını oluşturuyor.

Nasıl? Uygulamanın hiçbir yerinde FirebaseAnalytics.instance doğrudan çağrılmıyor. Tüm olaylar AnalyticsManager adlı tek bir sınıftan geçiyor:

await AnalyticsManager.logEvent(
AnalyticsConstants.outfitViewed,
parameters: {'outfit_id': outfit.id},
);
await AnalyticsManager.logScreenView(AnalyticsConstants.screenHome);
await AnalyticsManager.setUserProperties(userId: user.id);

Bu yaklaşımın üç faydası var:

  • Tutarlı isimlendirme. Olay adları AnalyticsConstants içinde sabit. Biri outfit_view, diğeri outfitViewed yazamaz. Analytics'te bir olay adını sonradan değiştirmek, geçmiş veriyle bağlantıyı koparır.
  • Tek açma/kapama noktası. setAnalyticsEnabled(false) ile kullanıcı onayı (GDPR/KVKK) tek satırda yönetilebiliyor.
  • Sağlayıcı bağımsızlığı. Yarın PostHog ya da başka bir araç eklemek istersek değişecek tek dosya bu.

Dikkat edilecek noktalar:

  • Ekran görüntülemeleri için logScreenView kullanın. Projede eski bir servis, ekranlar için page_view adında özel bir olay gönderiyordu. Mobilde Analytics zaten screen_view olayını tanır ve raporlarını bunun üzerine kurar. Özel bir olay bu raporlarda görünmez ve aynı bilgiyi iki farklı isimle saymaya başlarsınız.
  • Logout’ta kimliği temizleyin. clearUserProperties() setUserId(null) çağırır. Bunu yapmazsanız aynı cihazda giriş yapan ikinci kullanıcının olayları ilk kullanıcıya yazılır.
  • Kişisel veri göndermeyin. E-posta veya tam ad gibi bilgiler olay parametresi ya da user property olarak gönderilmemeli; bu Google Analytics kullanım koşullarına aykırıdır. Backend’in sayısal kullanıcı ID’si yeterli.
  • DebugView kullanın. Geliştirme sırasında olayları anında görmek için Android’de adb shell setprop debug.firebase.analytics.app <paket_adı>, iOS'ta -FIRDebugEnabled argümanını kullanın. Aksi halde olaylar konsola saatler sonra düşer.

10. Crashlytics: çökmeden önce ne oldu?

Neden? Bir çökme raporundaki stack trace size hatanın nerede olduğunu söyler, ama neden olduğunu çoğu zaman söylemez. Bu yüzden Crashlytics’i yalnızca çökmeleri toplayan bir araç olarak değil, çökmeden önceki son adımları kaydeden bir kara kutu olarak kurguladık.

Nasıl? Yine tek bir kapı. Analytics’te olduğu gibi uygulamanın hiçbir yerinde FirebaseCrashlytics.instance doğrudan çağrılmıyor. Her şey AppLogger adlı tek bir sınıftan geçiyor:

class AppLogger {
static void logEvent(String message) {
FirebaseCrashlytics.instance.log(message);
}

static void recordError(
Object exception,
StackTrace stack, {
String? reason,
bool fatal = false,
Map<String, Object>? customKeys,
}) {
customKeys?.forEach(FirebaseCrashlytics.instance.setCustomKey);
FirebaseCrashlytics.instance.recordError(
exception,
stack,
reason: reason,
fatal: fatal,
);
}

static Future<void> setUserIdentifier(String userId) =>
FirebaseCrashlytics.instance.setUserIdentifier(userId);
}

Bu sınıfla dört şey yapıyoruz:

  • Log satırları. log() ile yazılan mesajlar bir sonraki çökme ya da non-fatal hata raporuna eklenir. Ekran geçişlerini (Navigation: home → outfit_detail) ve önemli kullanıcı aksiyonlarını buraya yazıyoruz. Rapor açıldığında kullanıcının son adımları orada duruyor.
  • Custom key’ler. Uygulama sürümü, build numarası, platform, işletim sistemi sürümü ve dil gibi bilgileri açılışta bir kez key olarak yazıyoruz. “Bu sadece Android’de mi, sadece eski sürümde mi?” sorusunu konsolda bu key’lere göre filtreleyerek cevaplıyoruz.
  • Non-fatal hatalar. Uygulamayı çökertmeyen ama yanlış giden her şeyi (catch bloğuna düşen bir istek, beklenmeyen bir yanıt) recordError(..., fatal: false) ile gönderiyoruz. reason parametresi konsolda hatanın başlığı gibi çalışıyor.
  • Kullanıcı kimliği. setUserIdentifier ile backend'in sayısal kullanıcı ID'sini veriyoruz. "Uygulama bende çöküyor" diyen bir kullanıcının raporlarını bu ID ile bulabiliyoruz. E-posta ya da isim göndermiyoruz; Analytics'teki kural burada da geçerli.

Örnek: çökmeyen ama takılan splash ekranı. Bazı kullanıcılar splash ekranında takılı kalıyordu ama uygulama çökmüyordu. Çökme olmayınca rapor da olmuyordu. Çözüm, açılış akışının her adımını hem log satırı hem de custom key olarak yazmaktı:

void _trackSplashStep(String step, Duration elapsed, {Exception? error}) {
AppLogger.logEvent('Splash: $step');
FirebaseCrashlytics.instance
..setCustomKey('last_splash_step', step)
..setCustomKey('splash_duration_ms', elapsed.inMilliseconds);

if (error != null) {
AppLogger.recordError(
error,
StackTrace.current,
reason: 'Splash failed at $step',
customKeys: {'flow_step': step},
);
}
}

Splash zaman aşımına uğradığında bu fonksiyon bir hatayla çağrılıyor ve rapor non-fatal olarak gidiyor. Konsolda raporları last_splash_step değerine göre gruplayınca takılmanın hangi adımda yoğunlaştığı görünüyor. Çökmeyen bir sorunu, çökme aracıyla görünür hale getirmiş oluyoruz.

Get Kadriye Macit’s stories in your inbox

Join Medium for free to get updates from this writer.

Karşılaştığımız tuzak: hata yakalayıcıların birbirini ezmesi. FlutterError.onError ve PlatformDispatcher.instance.onError tek bir değer tutar; en son atanan kazanır. Bizde bu ikisi hem Crashlytics'in başlatıcı sınıfında hem de main.dart'ta atanıyordu. Kod çalışıyor gibi görünüyordu, ama başlatma sırası değiştiğinde hatalar sessizce kaybolabilirdi. Daha da kritik olanı: başlatıcıdaki PlatformDispatcher.onError hatayı hiçbir yere raporlamadan true dönüyordu; yani o sırada oluşan hataları yutuyordu. Kural: bu iki yakalayıcıyı tek bir yerde atayın.

Flutter için önerilen kurulum şu:

FlutterError.onError =
FirebaseCrashlytics.instance.recordFlutterFatalError;

PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};

Dikkat edilecek noktalar:

  • Debug’da toplamayı kapatın. setCrashlyticsCollectionEnabled(!kDebugMode) kullanın; aksi halde geliştirme sırasındaki hatalar prod panosunu doldurur.
  • Sembolleri yükleyin. Android’de com.google.firebase.crashlytics Gradle eklentisi mapping dosyalarını yükler; iOS'ta dSYM yüklemesini CI'a ekleyin. Flutter tarafında --obfuscate --split-debug-info kullanıyorsanız Dart sembollerini firebase crashlytics:symbols:upload ile ayrıca yüklemeniz gerekir. Yoksa stack trace'ler okunamaz.
  • Web’i ayrı düşünün. Crashlytics web’i desteklemiyor. Tek kapı burada da işe yarıyor: kIsWeb kontrolünü AppLoggeriçinde bir kez yapmak yeterli.

11. Sahadan hata-çözüm özeti

Seri boyunca anlattığım hataların hepsi bu tabloda. Bir sorunla karşılaştığınızda belirtiden geriye doğru arayabilirsiniz.

Press enter or click to view image in full size

12. Kapanış: yayına almadan önce kontrol listesi

Bu seriden tek bir cümle aklınızda kalacaksa şu olsun: Firebase’i kendi backend’inizle birlikte kullanmak çok güçlü bir model, ama iki dünyanın kimliği ve konfigürasyonu tek bir doğruluk kaynağına bağlanmadıkça hatalar sessizce birikir. Bizim yaşadığımız hataların neredeyse hepsi ya “iki kopya” ya da “sessizce yutulan bir catch" kaynaklıydı.

Kendi Flutter + Firebase projenizi yayına almadan önce şu listeyi kontrol edin:

Kurulum

  • Firebase.initializeApp tek bir yerde çağrılıyor; kritik olmayan işler ilk frame'den sonra.
  • Her ortamın ayrı Firebase projesi var ve config dosyaları flutterfire configure ile üretildi.
  • App Check açık; debug provider sadece kDebugMode'da, iOS'ta App Attest, web'de reCAPTCHA Enterprise.
  • Enforcement açılmadan önce doğrulanmış istek oranı izlendi.

Veri ve güvenlik

  • Firestore’a erişen her kullanıcının bir Firebase oturumu var (gerekirse custom token ile).
  • Security Rules if true içermiyor ve Emulator üzerinde test ediliyor.
  • Kurallar, henüz var olmayan belgeleri (resource == null) ayrıca ele alıyor.
  • list kuralları yalnızca sorgunun kanıtlayabileceği koşulları içeriyor; kurallar filtre değildir.
  • Retry edilen yazmalar idempotent; birden fazla yazma WriteBatch ya da transaction içinde.
  • Dinleyicilerde limit var; snapshot verisi tekrar get() ile okunmuyor.
  • Güvenilmesi gereken sayaçlar (örneğin beğeni) sunucuda güncelleniyor.

Kimlik

  • Apple: nonce doğru yönde, isim ilk girişte kaydediliyor.
  • Google: her flavor için doğru Web client ID ve iOS URL şeması.
  • Logout’ta Firebase Auth, Google Sign-In, Analytics kullanıcı ID’si ve FCM token kaydı temizleniyor.

Bildirimler ve gözlemlenebilirlik

  • Bildirim izni ilk frame’den sonra isteniyor; Android ikonu tek renkli.
  • Arka plan handler’ı top-level, @pragma('vm:entry-point') ile işaretli ve bir kez kaydediliyor.
  • Ön plan, arka plan ve kapalı durumları hem notification hem data mesajlarıyla test edildi.
  • Remote Config’te kısa fetchTimeout, kodda varsayılanlar ve JSON için güvenli okuma var.
  • Zorunlu güncelleme, minimum_app_version yükseltilerek gerçek bir cihazda test edildi.
  • Hata yakalayıcılar tek bir yerde atanıyor; hiçbir catch hatayı raporlamadan yutmuyor.

Bu yazının arkasındaki uygulama: Apparelte

Bu serideki bütün örnekler Apparelte’nin kodundan geliyor. Apparelte, kombinlerin alışveriş linkleriyle birlikte paylaşıldığı bir sosyal moda uygulaması. Beğendiğin bir kombini gördüğünde parçaların nereden alınabileceğini de hemen görüyorsun.

👉 apparelte.com üzerinden göz atabilir, uygulamayı indirip kendi kombinlerini paylaşabilirsin.

Sorularını ya da kendi Firebase hikâyelerini X’te bekliyorum: @kadriyemct

Buraya kadar okuduğun için teşekkürler. Umarım bir sonraki Firebase projeni canlıya alırken işine yarar. 🙌

--

--

Kadriye Macit

Written by Kadriye Macit

Sr. Flutter Developer - Software Engineer || Google Developer Expert for Flutter&Dart and Firebase