Menerima pesan di aplikasi Flutter

Bergantung pada status perangkat, pesan masuk akan ditangani secara berbeda. Untuk memahami skenario ini dan cara mengintegrasikan FCM ke dalam aplikasi Anda sendiri, sangat penting untuk menetapkan berbagai status perangkat terlebih dahulu:

Status Deskripsi
Latar depan Saat aplikasi dibuka, dilihat, dan digunakan.
Latar belakang Saat aplikasi dibuka, tetapi di latar belakang (diminimalkan). Hal ini biasanya terjadi saat pengguna menekan tombol "layar utama" di perangkat, beralih ke aplikasi lain menggunakan pengalih aplikasi, atau membuka aplikasi di tab yang berbeda (web).
Dihentikan Saat perangkat terkunci atau aplikasi tidak berjalan.

Ada beberapa prasyarat yang harus dipenuhi sebelum aplikasi dapat menerima payload pesan melalui FCM:

  • Aplikasi harus sudah dibuka setidaknya satu kali (untuk memungkinkan pendaftaran ke FCM).
  • Di iOS, jika pengguna menutup aplikasi dari pengalih aplikasi, aplikasi harus dibuka kembali secara manual agar pesan di latar belakang mulai berfungsi lagi.
  • Di Android, jika pengguna memaksa keluar dari aplikasi dari setelan perangkat, aplikasi harus dibuka kembali secara manual agar pesan mulai berfungsi.
  • Di web, Anda harus sudah meminta token (menggunakan getToken()) dengan sertifikat web push Anda.

Meminta izin untuk menerima pesan

Di iOS, macOS, web, dan Android 13 (atau yang lebih baru), sebelum payload FCM dapat diterima di perangkat Anda, Anda harus terlebih dahulu meminta izin pengguna.

Paket firebase_messaging menyediakan API sederhana untuk meminta izin melalui metode requestPermission. API ini menerima sejumlah argumen bernama yang menentukan jenis izin yang ingin Anda minta, seperti apakah pesan yang berisi payload notifikasi dapat memicu suara atau membaca pesan melalui Siri. Secara default, metode ini meminta izin default yang logis. API referensi memberikan dokumentasi lengkap tentang fungsi dari setiap izin.

Untuk memulai, panggil metode tersebut dari aplikasi Anda (di iOS, modal native akan ditampilkan, sedangkan di web, alur API native browser akan dipicu):

FirebaseMessaging messaging = FirebaseMessaging.instance;

NotificationSettings settings = await messaging.requestPermission(
  alert: true,
  announcement: false,
  badge: true,
  carPlay: false,
  criticalAlert: false,
  provisional: false,
  sound: true,
);

print('User granted permission: ${settings.authorizationStatus}');

Properti authorizationStatus dari objek NotificationSettings yang ditampilkan dari permintaan dapat digunakan untuk menentukan keputusan keseluruhan pengguna:

  • authorized: Pengguna memberikan izin.
  • denied: Pengguna menolak izin.
  • notDetermined: Pengguna belum memilih apakah akan memberikan izin atau tidak.
  • provisional: Pengguna memberikan izin sementara

Properti lainnya di NotificationSettings akan menampilkan apakah izin tertentu diaktifkan, dinonaktifkan, atau tidak didukung di perangkat saat ini.

Setelah izin diberikan dan berbagai jenis status perangkat dipahami, aplikasi Anda kini dapat mulai menangani payload FCM yang masuk.

Penanganan pesan

Berdasarkan status aplikasi Anda saat ini, payload yang masuk dari berbagai jenis pesan memerlukan implementasi yang berbeda untuk menanganinya:

Pesan latar depan

Untuk menangani pesan saat aplikasi berada di latar depan, proses aliran data onMessage.

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print('Got a message whilst in the foreground!');
  print('Message data: ${message.data}');

  if (message.notification != null) {
    print('Message also contained a notification: ${message.notification}');
  }
});

Aliran data berisi RemoteMessage, yang memerinci berbagai informasi tentang payload, seperti asalnya, ID unik, waktu pengiriman, apakah payload berisi notifikasi, dan lainnya. Karena pesan diambil saat aplikasi berada di latar depan, Anda dapat langsung mengakses status dan konteks aplikasi Flutter.

Pesan Notifikasi dan Latar Depan

Pesan notifikasi yang masuk saat aplikasi berada di latar depan secara default tidak akan menampilkan notifikasi yang terlihat, baik di Android maupun iOS. Namun, Anda dapat mengganti perilaku ini:

  • Di Android, Anda harus membuat saluran notifikasi "Prioritas Tinggi".
  • Di iOS, Anda dapat memperbarui opsi tampilan untuk aplikasi.

Pesan latar belakang

Proses penanganan pesan latar belakang berbeda pada platform native (Android dan Apple) dan platform berbasis web.

Platform Apple dan Android

Tangani pesan latar belakang dengan mendaftarkan pengendali onBackgroundMessage. Saat pesan diterima, isolate muncul (khusus Android, iOS/macOS tidak memerlukan isolate terpisah) yang memungkinkan Anda menangani pesan bahkan saat aplikasi tidak berjalan.

Ada beberapa hal yang perlu diingat tentang pengendali pesan latar belakang:

  1. Pengendali tidak boleh berupa fungsi anonim.
  2. Pengendali harus berupa fungsi tingkat atas (misalnya, bukan metode class yang memerlukan inisialisasi).
  3. Saat menggunakan Flutter versi 3.3.0 atau yang lebih baru, pengendali pesan harus dianotasi dengan @pragma('vm:entry-point') tepat di atas deklarasi fungsi (jika tidak, akan dihapus selama tree shaking untuk mode rilis).
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp();

  print("Handling a background message: ${message.messageId}");
}

void main() {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}

Karena pengendali berjalan di isolate miliknya sendiri di luar konteks aplikasi, Anda tidak dapat memperbarui status aplikasi atau menjalankan logika yang memengaruhi UI. Namun, Anda dapat menjalankan logika seperti permintaan HTTP, menjalankan operasi IO (misalnya, memperbarui penyimpanan lokal), berkomunikasi dengan plugin lain, dll.

Sebaiknya Anda menyelesaikan logika sesegera mungkin. Menjalankan tugas berdurasi lama dan secara intensif akan memengaruhi performa perangkat dan dapat menyebabkan OS menghentikan prosesnya. Jika tugas berjalan lebih dari 30 detik, perangkat mungkin akan otomatis menghentikan prosesnya.

Web

Di Web, tulis Service Worker JavaScript yang berjalan di latar belakang. Gunakan service worker untuk menangani pesan latar belakang.

Untuk memulai, buat file baru di direktori web, yaitu file firebase-messaging-sw.js:

// Please see this file for the latest firebase-js-sdk version:
// https://github.com/firebase/flutterfire/blob/master/packages/firebase_core/firebase_core_web/lib/src/firebase_sdk_version.dart
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js");

firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
});

const messaging = firebase.messaging();

// Optional:
messaging.onBackgroundMessage((message) => {
  console.log("onBackgroundMessage", message);
});

File tersebut harus mengimpor SDK aplikasi dan pesan, melakukan inisialisasi Firebase, dan mengekspos variabel messaging.

Selanjutnya, worker harus didaftarkan. Dalam file index.html, daftarkan worker dengan mengubah tag <script> yang melakukan bootstrap pada Flutter:

<script src="flutter_bootstrap.js" async>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
      navigator.serviceWorker.register('firebase-messaging-sw.js', {
        scope: '/firebase-cloud-messaging-push-scope',
      });
    });
  }
</script>

Jika masih menggunakan sistem template lama, Anda dapat mendaftarkan worker dengan mengubah tag <script> yang melakukan bootstrap pada Flutter sebagai berikut:

<html>
<body>
  <script>
      var serviceWorkerVersion = null;
      var scriptLoaded = false;
      function loadMainDartJs() {
        if (scriptLoaded) {
          return;
        }
        scriptLoaded = true;
        var scriptTag = document.createElement('script');
        scriptTag.src = 'main.dart.js';
        scriptTag.type = 'application/javascript';
        document.body.append(scriptTag);
      }

      if ('serviceWorker' in navigator) {
        // Service workers are supported. Use them.
        window.addEventListener('load', function () {
          // Register Firebase Messaging service worker.
          navigator.serviceWorker.register('firebase-messaging-sw.js', {
            scope: '/firebase-cloud-messaging-push-scope',
          });

          // Wait for registration to finish before dropping the <script> tag.
          // Otherwise, the browser will load the script multiple times,
          // potentially different versions.
          var serviceWorkerUrl =
            'flutter_service_worker.js?v=' + serviceWorkerVersion;

          navigator.serviceWorker.register(serviceWorkerUrl).then((reg) => {
            function waitForActivation(serviceWorker) {
              serviceWorker.addEventListener('statechange', () => {
                if (serviceWorker.state == 'activated') {
                  console.log('Installed new service worker.');
                  loadMainDartJs();
                }
              });
            }
            if (!reg.active && (reg.installing || reg.waiting)) {
              // No active web worker and we have installed or are installing
              // one for the first time. Simply wait for it to activate.
              waitForActivation(reg.installing ?? reg.waiting);
            } else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
              // When the app updates the serviceWorkerVersion changes, so we
              // need to ask the service worker to update.
              console.log('New service worker available.');
              reg.update();
              waitForActivation(reg.installing);
            } else {
              // Existing service worker is still good.
              console.log('Loading app from service worker.');
              loadMainDartJs();
            }
          });

          // If service worker doesn't succeed in a reasonable amount of time,
          // fallback to plaint <script> tag.
          setTimeout(() => {
            if (!scriptLoaded) {
              console.warn(
                'Failed to load app from service worker. Falling back to plain <script> tag.'
              );
              loadMainDartJs();
            }
          }, 4000);
        });
      } else {
        // Service workers not supported. Just drop the <script> tag.
        loadMainDartJs();
      }
  </script>
</body>

Selanjutnya, mulai ulang aplikasi Flutter Anda. Worker akan terdaftar dan pesan latar belakang akan ditangani melalui file ini.

Menangani Interaksi

Karena notifikasi adalah tanda yang terlihat, biasanya pengguna berinteraksi dengannya (dengan menekan). Perilaku default pada Android dan iOS adalah membuka aplikasi. Jika dihentikan, aplikasi akan dimulai; jika di latar belakang, aplikasi akan dialihkan ke latar depan.

Bergantung pada konten notifikasi, Anda mungkin ingin menangani interaksi pengguna saat aplikasi terbuka. Misalnya, jika pesan chat baru dikirim melalui notifikasi dan pengguna menekannya, sebaiknya Anda membuka percakapan tertentu saat aplikasi terbuka.

Paket firebase-messaging menyediakan dua cara untuk menangani interaksi ini:

  • getInitialMessage(): Jika aplikasi dibuka dari status dihentikan, Future yang berisi RemoteMessage akan ditampilkan. Setelah digunakan, RemoteMessage akan dihapus.
  • onMessageOpenedApp: Stream yang memposting RemoteMessage saat aplikasi dibuka dari status latar belakang.

Sebaiknya kedua skenario ditangani untuk memastikan UX yang lancar bagi pengguna Anda. Contoh kode di bawah ini menguraikan cara melakukannya:

class Application extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _Application();
}

class _Application extends State<Application> {
  // It is assumed that all messages contain a data field with the key 'type'
  Future<void> setupInteractedMessage() async {
    // Get any messages which caused the application to open from
    // a terminated state.
    RemoteMessage? initialMessage =
        await FirebaseMessaging.instance.getInitialMessage();

    // If the message also contains a data property with a "type" of "chat",
    // navigate to a chat screen
    if (initialMessage != null) {
      _handleMessage(initialMessage);
    }

    // Also handle any interaction when the app is in the background via a
    // Stream listener
    FirebaseMessaging.onMessageOpenedApp.listen(_handleMessage);
  }

  void _handleMessage(RemoteMessage message) {
    if (message.data['type'] == 'chat') {
      Navigator.pushNamed(context, '/chat',
        arguments: ChatArguments(message),
      );
    }
  }

  @override
  void initState() {
    super.initState();

    // Run code required to handle interacted messages in an async function
    // as initState() must not be async
    setupInteractedMessage();
  }

  @override
  Widget build(BuildContext context) {
    return Text("...");
  }
}

Cara Anda menangani interaksi bergantung pada penyiapan aplikasi Anda. Contoh di atas menampilkan ilustrasi dasar menggunakan StatefulWidget.

Melokalkan Pesan

Anda dapat mengirim string yang dilokalkan dengan dua cara yang berbeda:

  • Simpan bahasa pilihan setiap pengguna di server Anda dan kirim notifikasi yang disesuaikan untuk setiap bahasa
  • Sematkan string yang dilokalkan di aplikasi Anda dan manfaatkan setelan lokal native sistem operasi

Berikut ini cara menggunakan metode kedua:

Android

  1. Tentukan pesan bahasa default Anda di resources/values/strings.xml:

    <string name="notification_title">Hello world</string>
    <string name="notification_message">This is a message</string>
    
  2. Tentukan terjemahan pesan di direktori values-language. Misalnya, tentukan pesan bahasa Prancis di resources/values-fr/strings.xml:

    <string name="notification_title">Bonjour le monde</string>
    <string name="notification_message">C'est un message</string>
    
  3. Di payload server, gunakan title_loc_key dan body_loc_key untuk pesan yang dilokalkan, bukan kunci title, message, serta body, dan tetapkan ke atribut name pesan yang ingin Anda tampilkan.

    Payload pesan akan terlihat seperti ini:

    {
      "data": {
        "title_loc_key": "notification_title",
        "body_loc_key": "notification_message"
      }
    }
    

iOS

  1. Tentukan pesan bahasa default Anda di Base.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Hello World";
    "NOTIFICATION_MESSAGE" = "This is a message";
    
  2. Tentukan terjemahan pesan di direktori language.lproj. Misalnya, tentukan pesan bahasa Prancis di fr.lproj/Localizable.strings:

    "NOTIFICATION_TITLE" = "Bonjour le monde";
    "NOTIFICATION_MESSAGE" = "C'est un message";
    

    Payload pesan akan terlihat seperti ini:

    {
      "data": {
        "title_loc_key": "NOTIFICATION_TITLE",
        "body_loc_key": "NOTIFICATION_MESSAGE"
      }
    }
    

Mengaktifkan ekspor data pengiriman pesan

Anda dapat mengekspor data pesan ke BigQuery untuk dianalisis lebih lanjut. Dengan BigQuery, Anda dapat menganalisis data menggunakan BigQuery SQL, mengekspornya ke penyedia cloud lain, atau menggunakan data tersebut untuk model ML kustom Anda. Ekspor ke BigQuery mencakup semua data yang tersedia untuk pesan, terlepas dari jenis pesan atau apakah pesan dikirim melalui API atau Notifications Composer.

Untuk mengaktifkan ekspor, pertama-tama ikuti langkah-langkah yang dijelaskan di sini, lalu ikuti petunjuk berikut:

Android

Anda dapat menggunakan kode berikut:

await FirebaseMessaging.instance.setDeliveryMetricsExportToBigQuery(true);

iOS

Untuk iOS, Anda perlu mengubah AppDelegate.m dengan konten berikut.

#import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h"
#import <Firebase/Firebase.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  // Override point for customization after application launch.
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
          fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  [[FIRMessaging extensionHelper] exportDeliveryMetricsToBigQueryWithMessageInfo:userInfo];
}

@end

Web

Untuk Web, Anda perlu mengubah service worker agar dapat menggunakan SDK versi v9. Versi v9 harus dipaketkan, sehingga Anda perlu menggunakan bundler seperti esbuild, misalnya, agar service worker dapat berfungsi. Lihat aplikasi contoh untuk mengetahui cara mencapainya.

Setelah bermigrasi ke SDK v9, Anda dapat menggunakan kode berikut:

import {
  experimentalSetDeliveryMetricsExportedToBigQueryEnabled,
  getMessaging,
} from 'firebase/messaging/sw';
...

const messaging = getMessaging(app);
experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, true);

Jangan lupa menjalankan yarn build untuk mengekspor service worker versi baru Anda ke folder web.

Menampilkan gambar dalam notifikasi di iOS

Di perangkat Apple, agar Notifikasi FCM masuk menampilkan gambar dari payload FCM, Anda harus menambahkan ekstensi layanan notifikasi tambahan dan mengonfigurasi aplikasi untuk menggunakannya.

Jika menggunakan autentikasi ponsel Firebase, Anda harus menambahkan pod Firebase Auth ke Podfile Anda.

Langkah 1 - Tambahkan ekstensi layanan notifikasi

  1. Di Xcode, klik File > New > Target...
  2. Modal akan menampilkan daftar kemungkinan target. Scroll ke bawah atau gunakan filter untuk memilih Notification Service Extension. Klik Next.
  3. Tambahkan nama produk (gunakan "ImageNotification" untuk mengikuti tutorial ini), setel bahasa ke Objective-C, lalu klik Finish.
  4. Aktifkan skema dengan mengklik Activate.

Langkah 2 - Tambahkan target ke Podfile

Pastikan ekstensi baru Anda memiliki akses ke pod Firebase/Messaging dengan menambahkannya di Podfile:

  1. Dari Navigator, buka Podfile: Pods > Podfile

  2. Scroll ke bagian bawah file, lalu tambahkan:

    target 'ImageNotification' do
      use_frameworks!
      pod 'Firebase/Auth' # Add this line if you are using FirebaseAuth phone authentication
      pod 'Firebase/Messaging'
    end
    
  3. Instal atau update pod Anda menggunakan pod install dari direktori ios atau macos.

Langkah 3 - Gunakan helper ekstensi

Pada tahap ini, semuanya akan tetap berjalan seperti biasa. Langkah terakhir adalah memanggil helper ekstensi.

  1. Dari navigator, pilih ekstensi ImageNotification Anda

  2. Buka file NotificationService.m.

  3. Di bagian atas file, impor FirebaseMessaging.h tepat setelah NotificationService.h seperti yang ditunjukkan di bawah.

    Ganti konten NotificationService.m dengan:

    #import "NotificationService.h"
    #import "FirebaseMessaging.h"
    #import "FirebaseAuth.h" // Add this line if you are using FirebaseAuth phone authentication
    #import <UIKit/UIKit.h> // Add this line if you are using FirebaseAuth phone authentication
    
    @interface NotificationService ()
    
    @property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
    @property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
    
    @end
    
    @implementation NotificationService
    
    /* Uncomment this if you are using Firebase Auth
    - (BOOL)application:(UIApplication *)app
                openURL:(NSURL *)url
                options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
      if ([[FIRAuth auth] canHandleURL:url]) {
        return YES;
      }
      return NO;
    }
    
    - (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
      for (UIOpenURLContext *urlContext in URLContexts) {
        [FIRAuth.auth canHandleURL:urlContext.URL];
      }
    }
    */
    
    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
        self.contentHandler = contentHandler;
        self.bestAttemptContent = [request.content mutableCopy];
    
        // Modify the notification content here...
        [[FIRMessaging extensionHelper] populateNotificationContent:self.bestAttemptContent withContentHandler:contentHandler];
    }
    
    - (void)serviceExtensionTimeWillExpire {
        // Called just before the extension will be terminated by the system.
        // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
        self.contentHandler(self.bestAttemptContent);
    }
    
    @end
    

Langkah 4 - Tambahkan gambar ke payload

Dalam payload notifikasi, Anda kini dapat menambahkan gambar. Baca dokumentasi iOS tentang cara membuat permintaan kirim. Perlu diingat bahwa ukuran gambar maksimum 300 KB diterapkan oleh perangkat.