Flutter'da Push Notifications: FCM ve Local Notifications

Push bildirimler iki kaynaktan gelir: sunucudan gelen FCM (Firebase Cloud Messaging) bildirimleri ve cihazın kendi gönderdiği yerel bildirimler. Bu yazıda her ikisini de Flutter'da nasıl kuracağınızı, arka plan mesajlarını nasıl işleyeceğinizi ve bildirime tıklandığında nasıl yönlendirme yapacağınızı ele alacağız.

FCM Kurulumu

// pubspec.yaml
// firebase_core: ^3.6.0
// firebase_messaging: ^15.1.3
// flutter_local_notifications: ^17.2.2

// main.dart — Firebase başlatma
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  // Arka plan mesaj handler'ı — top-level fonksiyon olmalı
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  // iOS için izin iste
  final messaging = FirebaseMessaging.instance;
  await messaging.requestPermission(
    alert: true,
    badge: true,
    sound: true,
    provisional: false,
  );

  runApp(const MyApp());
}

// Top-level — sınıf üyesi olamaz
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  debugPrint('Arka plan mesajı: ${message.messageId}');
  // Bildirim kaydı, badge güncelleme vb.
}

FCM Servis Sınıfı

class NotificationService {
  final FirebaseMessaging _messaging = FirebaseMessaging.instance;
  final FlutterLocalNotificationsPlugin _localNotifications =
      FlutterLocalNotificationsPlugin();

  Future<void> initialize() async {
    // Local notifications başlat
    await _localNotifications.initialize(
      const InitializationSettings(
        android: AndroidInitializationSettings('@mipmap/ic_launcher'),
        iOS: DarwinInitializationSettings(
          requestAlertPermission: false,
          requestBadgePermission: false,
          requestSoundPermission: false,
        ),
      ),
      onDidReceiveNotificationResponse: _onNotificationTap,
    );

    // Android bildirim kanalı
    await _localNotifications
        .resolvePlatformSpecificImplementation<
            AndroidFlutterLocalNotificationsPlugin>()
        ?.createNotificationChannel(const AndroidNotificationChannel(
          'high_importance_channel',
          'Önemli Bildirimler',
          description: 'Kritik uygulama bildirimleri',
          importance: Importance.high,
        ));

    // Ön planda gelen FCM mesajını local notification olarak göster
    FirebaseMessaging.onMessage.listen(_handleForegroundMessage);

    // Uygulama kapalıyken bildirime tıklama
    final initial = await _messaging.getInitialMessage();
    if (initial != null) _handleNotificationNavigation(initial.data);

    // Arka planda bildirime tıklama
    FirebaseMessaging.onMessageOpenedApp.listen(
      (message) => _handleNotificationNavigation(message.data),
    );
  }

  Future<String?> getToken() => _messaging.getToken();

  void _handleForegroundMessage(RemoteMessage message) {
    final notification = message.notification;
    if (notification == null) return;

    _localNotifications.show(
      notification.hashCode,
      notification.title,
      notification.body,
      NotificationDetails(
        android: AndroidNotificationDetails(
          'high_importance_channel',
          'Önemli Bildirimler',
          icon: '@mipmap/ic_launcher',
          importance: Importance.high,
          priority: Priority.high,
          styleInformation: BigTextStyleInformation(notification.body ?? ''),
        ),
        iOS: const DarwinNotificationDetails(
          presentAlert: true,
          presentBadge: true,
          presentSound: true,
        ),
      ),
      payload: jsonEncode(message.data),
    );
  }

  void _onNotificationTap(NotificationResponse response) {
    if (response.payload == null) return;
    final data = jsonDecode(response.payload!) as Map<String, dynamic>;
    _handleNotificationNavigation(data);
  }

  void _handleNotificationNavigation(Map<String, dynamic> data) {
    final type = data['type'] as String?;
    final id = data['id'] as String?;
    if (type == null || id == null) return;

    switch (type) {
      case 'product':
        AppRouter.pushNamed('/products/$id');
      case 'order':
        AppRouter.pushNamed('/orders/$id');
      case 'chat':
        AppRouter.pushNamed('/messages/$id');
    }
  }

  // Topic'e abone ol
  Future<void> subscribeToTopic(String topic) =>
      _messaging.subscribeToTopic(topic);

  Future<void> unsubscribeFromTopic(String topic) =>
      _messaging.unsubscribeFromTopic(topic);
}

Local Notifications: Zamanlanmış Bildirimler

class ScheduledNotificationService {
  final FlutterLocalNotificationsPlugin _plugin =
      FlutterLocalNotificationsPlugin();

  // Belirli bir saatte bildirim gönder
  Future<void> scheduleReminder({
    required int id,
    required String title,
    required String body,
    required DateTime scheduledTime,
    String? payload,
  }) async {
    final tz = tz.TZDateTime.from(scheduledTime, tz.local);

    await _plugin.zonedSchedule(
      id,
      title,
      body,
      tz,
      NotificationDetails(
        android: AndroidNotificationDetails(
          'reminders',
          'Hatırlatıcılar',
          importance: Importance.high,
          priority: Priority.high,
        ),
        iOS: const DarwinNotificationDetails(),
      ),
      payload: payload,
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
    );
  }

  // Her gün aynı saatte bildirim
  Future<void> scheduleDailyAt({
    required int id,
    required String title,
    required String body,
    required Time time,
  }) async {
    await _plugin.periodicallyShowWithDuration(
      id, title, body,
      const Duration(days: 1),
      NotificationDetails(
        android: AndroidNotificationDetails('daily', 'Günlük'),
      ),
    );
  }

  // Bildirimi iptal et
  Future<void> cancel(int id) => _plugin.cancel(id);
  Future<void> cancelAll() => _plugin.cancelAll();

  // Bekleyen bildirimleri listele
  Future<List<PendingNotificationRequest>> getPending() =>
      _plugin.pendingNotificationRequests();
}

Android Manifest ve iOS Info.plist

<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

<application>
  <!-- FCM default notification icon -->
  <meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />
  <meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/notification_color" />
</application>

<!-- ios/Runner/Info.plist -->
<!-- Bu anahtarları ekleyin -->
<key>UIBackgroundModes</key>
<array>
  <string>fetch</string>
  <string>remote-notification</string>
</array>

Push bildirim sistemi, arka plan handler'ının top-level fonksiyon olma zorunluluğu ve iOS izin akışı nedeniyle kurulumu en karmaşık Flutter özelliklerinden biridir. Testler için Firebase console'un test mesajı gönderme aracını ve cihaz FCM token'ını kullanın.