↑↓ seç · Enter aç · Esc kapat

Mutlu Tekin
Mutlu Tekin
← Yazılar

Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)

5 dk okuma #flutter
Dizi · 43/61 Flutter Türkçe Tutorial
  1. Bölüm 1 — Konu 1: Flutter Nedir, Mimarisi (Widget → Element → RenderObject, Skia/Impeller)
  2. Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, DevTools'a İlk Bakış)
  3. Bölüm 1 — Konu 3: İlk Proje Yapısı (`pubspec.yaml`, `lib/`, Klasör Mimarisi, `flutter create` Anatomisi)
  4. Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli
  5. Bölüm 2 — Konu 5: "Her Şey Widget'tır" Felsefesi, Widget Ağacı
  6. Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + `setState()` Derinlemesine (Rebuild Mekanizması)
  7. Bölüm 2 — Konu 7: Temel Layout — Container, Row, Column, Stack, Padding, Align, Center
  8. Bölüm 2 — Konu 8: Constraint Sistemi — "Constraints Go Down, Sizes Go Up, Parent Sets Position"
  9. Bölüm 2 — Konu 9: `Expanded`, `Flexible`, `Spacer`, Intrinsic Widget'lar
  10. Bölüm 3 — Konu 10: MaterialApp/CupertinoApp, Scaffold, AppBar
  11. Bölüm 3 — Konu 11: `ListView`, `GridView`, `SingleChildScrollView` (+ Builder Pattern)
  12. Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi
  13. Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)
  14. Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes
  15. Bölüm 4 — Konu 15: Form, TextField/TextFormField, GlobalKey<FormState>, Validasyon
  16. Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)
  17. Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)
  18. Bölüm 5 — Konu 18: Future/Async-Await Flutter Bağlamında, FutureBuilder
  19. Bölüm 5 — Konu 19: `Stream`, `StreamBuilder`
  20. Bölüm 5 — Konu 20: HTTP İstekleri (`http` / `dio` Paketleri)
  21. Bölüm 5 — Konu 21: JSON Serialization (Manuel → `json_serializable`/`freezed`)
  22. Bölüm 5 — Konu 22: Local Storage — `shared_preferences` → `Hive` → `sqflite`/Drift
  23. Bölüm 6 — Konu 23: InheritedWidget ve InheritedModel — "Neden setState Yetmiyor" Sorusunun Cevabı
  24. Bölüm 6 — Konu 24: Provider Paketi
  25. Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)
  26. Bölüm 6 — Konu 26: BLoC/Cubit Pattern (`flutter_bloc`)
  27. Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)
  28. Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi?
  29. Bölüm 7 — Konu 29: Navigator 2.0 (Router, RouteInformationParser, RouterDelegate)
  30. Bölüm 7 — Konu 30: `go_router` Paketi (Pratik ve Modern Çözüm)
  31. Bölüm 7 — Konu 31: Deep Linking
  32. Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)
  33. Bölüm 7 — Konu 33: Dependency Injection (`get_it`, `injectable`)
  34. Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri
  35. Bölüm 8 — Konu 35: Implicit Animasyonlar
  36. Bölüm 8 — Konu 36: Explicit Animasyonlar (`AnimationController`, `Tween`, `Curve`)
  37. Bölüm 8 — Konu 37: Hero Animasyonları
  38. Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`
  39. Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu
  40. Bölüm 9 — Konu 40: Platform Channels
  41. Bölüm 9 — Konu 41: Permission Yönetimi (`permission_handler`)
  42. Bölüm 9 — Konu 42: Kamera, Konum, Sensörler
  43. Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)
  44. Bölüm 9 — Konu 44: Android/iOS Build Sistemleri
  45. Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)
  46. Bölüm 10 — Konu 46: CI/CD
  47. Bölüm 10 — Konu 47: Rebuild Optimizasyonu (`const`, `key` Kullanımı)
  48. Bölüm 10 — Konu 48: DevTools Profiling
  49. Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı
  50. Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)
  51. Bölüm 11 — Konu 51: Supabase Alternatifi
  52. Bölüm 11 — Konu 52: GraphQL (Opsiyonel)
  53. Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)
  54. Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi
  55. Bölüm 12 — Konu 55: Flutter Web / Desktop
  56. Bölüm 12 — Konu 56: Custom `RenderObject` Yazımı
  57. Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)
  58. Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama
  59. Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)
  60. Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine
  61. Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n)
Dizinin sayfası →
İçindekiler 13 başlık
  1. Genel Akış — Kim Kiminle Konuşuyor?
  2. Kurulum — Firebase Projesi Bağlama
  3. İzin İsteme — iOS İçin Zorunlu
  4. FCM Token — Cihazın "Adresi"
  5. Üç Farklı Uygulama Durumu, Üç Farklı Davranış
  6. 1. Uygulama Ön Planda (Foreground) — onMessage
  7. 2. Uygulama Arka Planda (Background) — onMessageOpenedApp
  8. 3. Uygulama Tamamen Kapalı (Terminated) — getInitialMessage
  9. Ön Planda Bildirim Gösterme — flutter_local_notifications
  10. Arka Plan Mesaj İşleyicisi — Neden main() Dışında, Üst Seviyede Olmalı?
  11. Konu Bazlı Yayın (Topics) — Toplu Bildirim
  12. 🎯 Bu Dersten Çıkarılması Gerekenler
  13. 📝 Ödevler

Bölüm 9'un son konusuna geldik. Şimdiye kadar öğrendiğimiz her şey, uygulama açıkken kullanıcıyla etkileşimdi. Push notification, bunun tam tersi bir senaryo: uygulama kapalıyken bile, sunucudan kullanıcıya bir mesaj ulaştırmak. Bunun için Flutter ekosisteminde en yaygın kullanılan araç Firebase Cloud Messaging (FCM)'dir.

Genel Akış — Kim Kiminle Konuşuyor?

metin
Senin Sunucun (veya Firebase Console)
        │
        │  "Bu kullanıcıya şu mesajı gönder"
        ▼
   Firebase Cloud Messaging (FCM) sunucuları
        │
        │  Bildirimi cihaza iletir
        ▼
  Kullanıcının Telefonu (uygulama açık/kapalı/arka planda fark etmez)

Her cihazdaki her uygulama kurulumu, FCM tarafından benzersiz bir "token" ile tanımlanır — sunucun, "şu token'a sahip cihaza bildirim gönder" diyerek belirli bir kullanıcıya ulaşır. Bu, Bölüm 5 Konu 20'de öğrendiğimiz HTTP isteklerinin tam tersi yönde çalışan bir mekanizma — orada Dart, sunucuya istek gönderiyordu; burada sunucu, Dart'a (FCM üzerinden) veri itiyor (bu yüzden "push").

Kurulum — Firebase Projesi Bağlama

bash
flutter pub add firebase_core firebase_messaging

Paketleri eklemek teknik başlangıç — asıl kurulum, flutterfire configure komutuyla (Firebase CLI aracı) yapılır; bu komut, Firebase konsolunda oluşturduğun projeyi, Android/iOS platform dosyalarına (google-services.json, GoogleService-Info.plist) otomatik olarak bağlar. Bu adım tutorial'ın kapsamı dışında (Firebase konsolu kullanımı gerektirir) ama akışı bilmen önemli.

dart
// main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // async main() öncesi gerekli
  await Firebase.initializeApp();
  runApp(const MyApp());
}

WidgetsFlutterBinding.ensureInitialized() — Dart Bölüm 5'te öğrendiğimiz async/await'i main() içinde kullanabilmek için gereken bir hazırlık çağrısı; Flutter'ın widget motorunu, herhangi bir asenkron işlem (Firebase bağlantısı gibi) yapılmadan önce başlatır.

İzin İsteme — iOS İçin Zorunlu

dart
Future<void> bildirimIzniIste() async {
  final ayarlar = await FirebaseMessaging.instance.requestPermission(
    alert: true,
    badge: true,
    sound: true,
  );

  if (ayarlar.authorizationStatus == AuthorizationStatus.authorized) {
    print('Kullanıcı bildirimlere izin verdi');
  }
}

Platform farkı önemli: Android'de (13 öncesi) bildirim izni otomatik verilmiş sayılırdı; Android 13'ten (API 33) itibaren, Android da çalışma zamanında izinister hale geldi — yani Konu 41'de öğrendiğimiz POST_NOTIFICATIONS izniyle aynı mantığa girdi. iOS'ta ise bu izin her zaman zorunluydu. firebase_messaging'in requestPermission()'ı, her iki platformda da tutarlı çalışan tek bir API sunuyor — Bölüm 9'un başından beri gördüğümüz "platforma özgü karmaşıklığı, tek bir Dart API'sinin arkasına gizleme" desenin bir başka örneği.

FCM Token — Cihazın "Adresi"

dart
Future<String?> tokenAl() async {
  final token = await FirebaseMessaging.instance.getToken();
  print('FCM Token: $token');
  return token;
}

Bu token'ı, uygulamanın kendi sunucusuna (Bölüm 5 Konu 20'de öğrendiğimiz http/dio ile) göndermen gerekir — sunucun, hangi token'ın hangi kullanıcıya ait olduğunu kendi veritabanında eşleştirip saklar, ileride o kullanıcıya bildirim göndermek istediğinde bu token'ı kullanır.

Üç Farklı Uygulama Durumu, Üç Farklı Davranış

Push notification'ların en kafa karıştırıcı yanı, uygulamanın hangi durumda olduğuna göre (açık/arka planda/tamamen kapalı) farklı kod yollarının çalışmasıdır:

1. Uygulama Ön Planda (Foreground) — onMessage

dart
FirebaseMessaging.onMessage.listen((RemoteMessage mesaj) {
  print('Ön planda mesaj geldi: ${mesaj.notification?.title}');
  // Dikkat: uygulama açıkken, sistem OTOMATİK bir bildirim GÖSTERMEZ!
  // Kendi bildirim UI'ını göstermen gerekir (aşağıda göreceğiz).
});

Kritik ve sık yapılan bir hata kaynağı: Uygulama açıkken bir push notification gelirse, işletim sistemi varsayılan olarak bildirimi ekranda göstermez — sadece onMessage stream'ine bir olay düşer. Bunun mantığı var: kullanıcı zaten uygulamanın içindeyse, sistem bildirimi yerine uygulama içi bir gösterim (bir SnackBar, bir banner) daha doğru bir kullanıcı deneyimidir. Yine de sistem bildirimi gibi görünen bir şey göstermek istersen, flutter_local_notifications paketiyle elle tetiklersin (birazdan göreceğiz).

2. Uygulama Arka Planda (Background) — onMessageOpenedApp

dart
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage mesaj) {
  print('Kullanıcı bildirime tıklayıp uygulamayı açtı: ${mesaj.data}');
  // Bölüm 7 Konu 30'u hatırla — go_router ile ilgili ekrana yönlendir
  // router.go('/urun/${mesaj.data['urunId']}');
});

Uygulama arka plandayken (kapatılmamış ama görünür değil) bir bildirim gelirse, sistem otomatik olarak bildirimi gösterir. Kullanıcı bildirime tıklayıp uygulamayı öne getirdiğinde, onMessageOpenedApp tetiklenir — bu, Bölüm 7'de öğrendiğimiz deep linking ile doğal bir birleşim noktasıdır: bildirimin içindeki veriye (mesaj.data) göre, kullanıcıyı doğrudan ilgili ekrana yönlendirebilirsin.

3. Uygulama Tamamen Kapalı (Terminated) — getInitialMessage

dart
Future<void> baslangicMesajiniKontrolEt() async {
  final RemoteMessage? mesaj = await FirebaseMessaging.instance.getInitialMessage();
  if (mesaj != null) {
    print('Uygulama, bir bildirime tıklanarak AÇILDI: ${mesaj.data}');
    // yine ilgili ekrana yönlendirme mantığı
  }
}

Uygulama tamamen kapalıyken bir bildirime tıklanırsa, uygulama sıfırdan başlar — onMessageOpenedApp bu senaryoda tetiklenmez (çünkü uygulama zaten çalışmıyordu, dinleyecek bir şey yoktu). Bunun yerine, main() çalıştıktan sonra getInitialMessage() ile "beni bir bildirim mi başlattı?" diye elle kontrol etmen gerekir.

Bu üçünü neden ayrı ayrı ele almak zorundayız? Çünkü Flutter/Dart kodu, uygulama kapalıyken çalışmıyor — her senaryo, işletim sisteminin kendi bildirim mekanizmasıyla farklı şekilde etkileşime giriyor, ve Flutter bunu üç farklı API ile sana yansıtıyor.

Ön Planda Bildirim Gösterme — flutter_local_notifications

bash
flutter pub add flutter_local_notifications
dart
final _yerelBildirimler = FlutterLocalNotificationsPlugin();

Future<void> onPlandaBildirimGoster(RemoteMessage mesaj) async {
  const androidDetay = AndroidNotificationDetails(
    'onemli_kanal', // kanal ID (Android 8+ için zorunlu)
    'Önemli Bildirimler',
    importance: Importance.high,
  );
  const detaylar = NotificationDetails(android: androidDetay);

  await _yerelBildirimler.show(
    mesaj.hashCode, // bildirim ID'si (Dart Bölüm 6 — her nesnenin hashCode'u vardır)
    mesaj.notification?.title,
    mesaj.notification?.body,
    detaylar,
  );
}

// onMessage dinleyicisinde çağır:
FirebaseMessaging.onMessage.listen(onPlandaBildirimGoster);

Bu paket, FCM'den bağımsız olarak da çalışabilen, yerel (cihazda anlık oluşturulan) bildirimler göstermeni sağlar — burada onu, "ön planda gelen FCM mesajını, sanki sistem bildirimi gibi göster" amacıyla kullanıyoruz. NotificationDetails, Dart Bölüm 6'da öğrendiğimiz named parametreler ile platforma özgü ayarları (Android kanalı, iOS ses/rozet ayarları) bir arada tutan bir yapılandırma nesnesi.

Arka Plan Mesaj İşleyicisi — Neden main() Dışında, Üst Seviyede Olmalı?

dart
@pragma('vm:entry-point')
Future<void> arkaPlanMesajIsleyici(RemoteMessage mesaj) async {
  print('Arka planda/kapalıyken mesaj işleniyor: ${mesaj.messageId}');
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(arkaPlanMesajIsleyici);
  runApp(const MyApp());
}

@pragma('vm:entry-point') — bu garip görünen anotasyon zorunludur, çünkü arka plan işleyicisi, Dart Bölüm 13'te öğrendiğimiz isolate kavramını hatırlarsan, ayrı bir isolate'te, hatta uygulamanın kendisi kapalıyken bile çalıştırılabilir. Dart'ın derleyicisi, normalde "kullanılmayan" kodu optimize ederek kaldırabilir (tree-shaking) — @pragma('vm:entry-point'), derleyiciye "bu fonksiyonu silme, dışarıdan (native taraftan) çağrılacak" demenin yolu. Fonksiyonun üst seviye (top-level, bir sınıfın içinde değil) tanımlanması da aynı sebepten zorunlu — ayrı bir isolate'in, bir widget'ın state'ine ya da closure'a erişimi olmaz (Dart Bölüm 13'teki isolate izolasyonunu hatırla).

Konu Bazlı Yayın (Topics) — Toplu Bildirim

dart
await FirebaseMessaging.instance.subscribeToTopic('kampanyalar');
// artık sunucu, TÜM 'kampanyalar' abonelerine TEK bir çağrıyla bildirim gönderebilir

await FirebaseMessaging.instance.unsubscribeFromTopic('kampanyalar');

Her kullanıcıya tek tek token ile bildirim göndermek yerine (örneğin "yeni bir kampanya başladı" gibi herkese gidecek bir bildirim için), kullanıcıları bir "konuya" (topic) abone edip, sunucunun tek bir çağrıyla o konudaki herkese ulaşmasını sağlayabilirsin — bir e-posta bülteni aboneliğine kavramsal olarak çok benzer.


🎯 Bu Dersten Çıkarılması Gerekenler

  • FCM, sunucunun bir cihaza kendi FCM token'ı üzerinden bildirim "itmesini" (push) sağlar; token, sunucunun tarafında kullanıcıyla eşleştirilip saklanmalıdır.
  • Uygulamanın durumuna göre üç farklı API devreye girer: onMessage (ön planda), onMessageOpenedApp (arka plandan tıklanarak açılma), getInitialMessage() (tamamen kapalıyken tıklanarak açılma).
  • Uygulama ön plandayken, sistem bildirimi otomatik göstermez — flutter_local_notifications ile elle göstermen gerekir.
  • Arka plan mesaj işleyicisi, üst seviyede tanımlanmalı ve @pragma('vm:entry-point') ile işaretlenmelidir — çünkü ayrı bir isolate'te, uygulama kapalıyken bile çalıştırılabilir.
  • subscribeToTopic(), tek tek token yönetmeden, toplu bildirim göndermeyi sağlar.

📝 Ödevler

  • [ ] Firebase projesi kurup (Firebase Console + flutterfire configure), uygulamana FCM'i entegre et, kendi FCM token'ını konsolda yazdır.
  • [ ] Firebase Console'dan (veya Postman ile FCM API'sine) test bir bildirim gönder; uygulama sırasıyla ön planda, arka planda ve kapalıyken, üç farklı davranışı gözlemle.
  • [ ] flutter_local_notifications ile, ön planda gelen bir FCM mesajını sistem bildirimi gibi gösteren kodu ekle.
  • [ ] Bir bildirime tıklandığında, mesaj.data içindeki bir ID'yi kullanarak go_router ile ilgili detay ekranına yönlendiren kodu yaz (hem onMessageOpenedApp hem getInitialMessage için).
  • [ ] Kendi cümlelerinle, "neden arka plan mesaj işleyicisinin üst seviyede tanımlanması gerektiğini", Dart'ın isolate kavramıyla ilişkilendirerek açıkla.

Sıradaki konu: Bölüm 9 — Konu 44: Android/iOS Build Sistemleri (Bölüm 9'un Son Konusu)