Tampilan
Bab 30 — Push Notification (FCM)
Push notification adalah cara aplikasimu menghubungi pengguna ketika aplikasi sedang tidak dibuka. Firebase Cloud Messaging menyediakannya gratis, tetapi pengaturannya menyentuh banyak lapisan — izin, token, tiga keadaan aplikasi yang berbeda, dan konfigurasi platform yang berbeda antara Android dan iOS.
Bab ini membahas semuanya secara berurutan.
Bagaimana push notification bekerja
SERVER / CLOUD FUNCTION
│
│ kirim ke: token / topik
▼
┌───────────────────┐
│ Firebase Cloud │
│ Messaging │
└─────────┬─────────┘
│
┌──────┴──────┐
▼ ▼
FCM APNs
(Android) (iOS)
│ │
└──────┬──────┘
▼
PERANGKAT PENGGUNA
│
┌─────────┴──────────┐
▼ ▼
Aplikasi terbuka? Aplikasi tertutup?
onMessage Sistem menampilkan
(kamu yang notifikasi sendiri
menampilkan)Dua hal penting yang perlu kamu pahami sejak awal:
Setiap perangkat punya token unik. Token inilah alamat pengiriman. Kamu harus menyimpannya ke Firestore agar server tahu ke mana mengirim.
Perilakunya berbeda tergantung keadaan aplikasi. Ini yang paling sering membingungkan, dan kita bahas terpisah di bawah.
Konfigurasi Android
yaml
# pubspec.yaml
dependencies:
firebase_messaging: ^15.1.5
flutter_local_notifications: ^18.0.1flutter_local_notifications diperlukan karena Android tidak menampilkan notifikasi secara otomatis ketika aplikasi sedang terbuka — kamu harus menampilkannya sendiri.
Izin notifikasi
Android 13 (API 33) ke atas membutuhkan izin runtime:
xml
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application ...>
<!-- Channel bawaan untuk notifikasi -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="notifikasi_penting" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
<activity ...>
<!-- Agar ketukan notifikasi membuka aplikasi -->
<intent-filter>
<action android:name="FLUTTER_NOTIFICATION_CLICK" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>Ikon notifikasi Android harus putih transparan
Ikon di @drawable/ic_notification harus berupa siluet putih dengan latar transparan. Kalau kamu memakai logo berwarna, Android akan menampilkannya sebagai kotak putih polos — bug visual yang membingungkan dan sering baru disadari setelah rilis.
Konfigurasi iOS
iOS lebih ketat. Push notification tidak berfungsi di simulator — kamu wajib menguji di perangkat fisik.
Langkahnya:
- Apple Developer Account berbayar diperlukan.
- Buat APNs Authentication Key (
.p8) di developer.apple.com → Certificates, Identifiers & Profiles → Keys. - Unggah berkas
.p8itu ke Firebase Console → Project Settings → Cloud Messaging → APNs Authentication Key. - Di Xcode, buka
Runner.xcworkspace→ tab Signing & Capabilities → tambahkan Push Notifications dan Background Modes (centang Remote notifications).
xml
<!-- ios/Runner/Info.plist -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>Layanan notifikasi
dart
// lib/data/services/layanan_notifikasi.dart
import 'dart:convert';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
/// Handler pesan latar belakang.
/// WAJIB berupa fungsi tingkat atas (bukan metode kelas)
/// dan diberi anotasi @pragma agar tidak dihapus saat kompilasi rilis.
@pragma('vm:entry-point')
Future<void> tanganiPesanLatarBelakang(RemoteMessage pesan) async {
// Isolate terpisah — jangan sentuh state aplikasi di sini.
debugPrint('Pesan latar belakang: ${pesan.messageId}');
}
class LayananNotifikasi {
LayananNotifikasi({FirebaseMessaging? messaging})
: _fcm = messaging ?? FirebaseMessaging.instance;
final FirebaseMessaging _fcm;
final _lokal = FlutterLocalNotificationsPlugin();
static const _channel = AndroidNotificationChannel(
'notifikasi_penting',
'Notifikasi Penting',
description: 'Pemberitahuan tentang polling dan aktivitas akunmu.',
importance: Importance.high,
);
/// Dipanggil ketika pengguna mengetuk notifikasi.
void Function(Map<String, dynamic> data)? saatDiketuk;
Future<void> inisialisasi() async {
// 1. Daftarkan handler latar belakang
FirebaseMessaging.onBackgroundMessage(tanganiPesanLatarBelakang);
// 2. Minta izin
await _mintaIzin();
// 3. Siapkan notifikasi lokal (untuk aplikasi yang sedang terbuka)
await _siapkanNotifikasiLokal();
// 4. Pasang pendengar
_pasangPendengar();
}
Future<bool> _mintaIzin() async {
final pengaturan = await _fcm.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
return pengaturan.authorizationStatus == AuthorizationStatus.authorized ||
pengaturan.authorizationStatus == AuthorizationStatus.provisional;
}
Future<void> _siapkanNotifikasiLokal() async {
const pengaturanAndroid =
AndroidInitializationSettings('@drawable/ic_notification');
const pengaturanIos = DarwinInitializationSettings(
requestAlertPermission: false, // sudah diminta lewat FCM
requestBadgePermission: false,
requestSoundPermission: false,
);
await _lokal.initialize(
const InitializationSettings(
android: pengaturanAndroid,
iOS: pengaturanIos,
),
onDidReceiveNotificationResponse: (respons) {
final payload = respons.payload;
if (payload == null) return;
saatDiketuk?.call(json.decode(payload) as Map<String, dynamic>);
},
);
// Android 8+ membutuhkan channel yang terdaftar
await _lokal
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(_channel);
// iOS: tampilkan notifikasi meskipun aplikasi terbuka
await _fcm.setForegroundNotificationPresentationOptions(
alert: true,
badge: true,
sound: true,
);
}
void _pasangPendengar() {
// Aplikasi sedang TERBUKA
FirebaseMessaging.onMessage.listen((pesan) {
final notif = pesan.notification;
if (notif == null) return;
// Android tidak menampilkannya otomatis — kita yang menampilkan
_lokal.show(
notif.hashCode,
notif.title,
notif.body,
NotificationDetails(
android: AndroidNotificationDetails(
_channel.id,
_channel.name,
channelDescription: _channel.description,
importance: Importance.high,
priority: Priority.high,
icon: '@drawable/ic_notification',
),
iOS: const DarwinNotificationDetails(),
),
payload: json.encode(pesan.data),
);
});
// Aplikasi di LATAR BELAKANG, lalu notifikasi diketuk
FirebaseMessaging.onMessageOpenedApp.listen((pesan) {
saatDiketuk?.call(pesan.data);
});
}
/// Notifikasi yang membuka aplikasi dari keadaan TERTUTUP.
Future<void> periksaPesanPembuka() async {
final pesan = await _fcm.getInitialMessage();
if (pesan != null) {
saatDiketuk?.call(pesan.data);
}
}
// --- Token ---
Future<String?> ambilToken() => _fcm.getToken();
Stream<String> get perubahanToken => _fcm.onTokenRefresh;
Future<void> hapusToken() => _fcm.deleteToken();
// --- Topik ---
Future<void> langgananTopik(String topik) => _fcm.subscribeToTopic(topik);
Future<void> berhentiLangganan(String topik) =>
_fcm.unsubscribeFromTopic(topik);
}Handler latar belakang harus fungsi tingkat atas
tanganiPesanLatarBelakang wajib:
- Berupa fungsi tingkat atas atau metode statis — bukan metode instance.
- Diberi anotasi
@pragma('vm:entry-point'), atau compiler rilis akan menghapusnya karena tampak tidak dipakai. - Tidak menyentuh state aplikasi — ia berjalan di isolate terpisah tanpa akses ke widget tree atau provider.
Kalau kamu butuh Firebase di dalamnya, panggil Firebase.initializeApp() lagi di sana.
Tiga keadaan aplikasi
Ini bagian yang paling sering menyebabkan kebingungan. Perilaku notifikasi berbeda tergantung apakah aplikasi sedang terbuka, di latar belakang, atau benar-benar tertutup.
| Keadaan | Android | iOS | Yang kamu tangani |
|---|---|---|---|
| Terbuka (foreground) | Tidak tampil otomatis | Tidak tampil otomatis¹ | onMessage — tampilkan sendiri |
| Latar belakang | Tampil otomatis | Tampil otomatis | onMessageOpenedApp saat diketuk |
| Tertutup (terminated) | Tampil otomatis | Tampil otomatis | getInitialMessage() saat dibuka |
¹ Kecuali kamu memanggil setForegroundNotificationPresentationOptions, seperti pada kode di atas.
Ketiganya harus ditangani, atau ketukan notifikasi akan mengarahkan pengguna ke halaman yang salah pada salah satu keadaan.
Mengelola token
Token adalah alamat pengiriman. Ia harus disimpan ke Firestore dan diperbarui ketika berubah.
dart
// lib/providers/penyedia_notifikasi.dart
import 'dart:async';
import 'package:flutter/material.dart';
import '../data/repositories/auth_repository.dart';
import '../data/services/layanan_notifikasi.dart';
class PenyediaNotifikasi extends ChangeNotifier {
PenyediaNotifikasi({
required LayananNotifikasi layanan,
required AuthRepository authRepo,
}) : _layanan = layanan,
_authRepo = authRepo;
final LayananNotifikasi _layanan;
final AuthRepository _authRepo;
StreamSubscription<String>? _langgananToken;
String? _token;
String? get token => _token;
Future<void> siapkanUntuk(String uid) async {
await _layanan.inisialisasi();
// Simpan token saat ini
_token = await _layanan.ambilToken();
if (_token != null) {
await _authRepo.perbaruiToken(uid, _token!);
}
// Token bisa berubah — misalnya setelah aplikasi dipasang ulang
_langgananToken = _layanan.perubahanToken.listen((tokenBaru) async {
_token = tokenBaru;
await _authRepo.perbaruiToken(uid, tokenBaru);
notifyListeners();
});
// Berlangganan topik umum
await _layanan.langgananTopik('semua_pengguna');
notifyListeners();
}
/// Dipanggil saat pengguna keluar akun.
Future<void> bersihkan() async {
await _layanan.berhentiLangganan('semua_pengguna');
await _layanan.hapusToken();
_token = null;
await _langgananToken?.cancel();
_langgananToken = null;
}
@override
void dispose() {
_langgananToken?.cancel();
super.dispose();
}
}Hapus token saat keluar akun
Kalau tidak, pengguna berikutnya yang memakai perangkat itu akan menerima notifikasi yang ditujukan untuk pengguna sebelumnya — kebocoran privasi yang nyata.
Menangani ketukan notifikasi
Notifikasi biasanya membawa data yang menentukan ke mana pengguna diarahkan.
dart
// lib/main.dart — di dalam widget yang punya akses Navigator
class AplikasiSaya extends StatefulWidget {
const AplikasiSaya({super.key});
@override
State<AplikasiSaya> createState() => _AplikasiSayaState();
}
class _AplikasiSayaState extends State<AplikasiSaya> {
final _kunciNavigator = GlobalKey<NavigatorState>();
@override
void initState() {
super.initState();
final layanan = context.read<LayananNotifikasi>();
layanan.saatDiketuk = _arahkan;
// Periksa apakah aplikasi dibuka OLEH notifikasi
WidgetsBinding.instance.addPostFrameCallback((_) {
layanan.periksaPesanPembuka();
});
}
void _arahkan(Map<String, dynamic> data) {
final tipe = data['tipe'] as String?;
final id = data['id'] as String?;
if (tipe == null || id == null) return;
switch (tipe) {
case 'poll':
_kunciNavigator.currentState?.pushNamed('/poll', arguments: id);
case 'chat':
_kunciNavigator.currentState?.pushNamed('/chat', arguments: id);
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: _kunciNavigator, // agar bisa navigasi dari luar widget
onGenerateRoute: _router.bangunRute,
home: const GerbangAuth(),
);
}
}navigatorKey diperlukan karena penanganan notifikasi terjadi di luar widget tree — kamu tidak punya BuildContext yang valid di sana. GlobalKey memberimu akses ke NavigatorState dari mana pun, sesuai penjelasan di Bab 12.
Mengirim notifikasi
Cara 1: dari Firebase Console
Untuk pengujian cepat: Firebase Console → Messaging → Create your first campaign → Firebase Notification messages.
Kamu bisa mengirim ke satu token tertentu (Send test message), ke topik, atau ke seluruh pengguna.
Cara 2: berlangganan topik
Topik adalah cara mengelompokkan penerima tanpa mengelola daftar token.
dart
// Pengguna berlangganan
await _fcm.subscribeToTopic('polling_baru');
await _fcm.subscribeToTopic('kota_bandung');
// Berhenti
await _fcm.unsubscribeFromTopic('polling_baru');Mengirim ke topik mengirimkannya ke semua yang berlangganan — tanpa kamu perlu tahu satu pun tokennya.
Kapan topik, kapan token
- Topik untuk siaran: pengumuman, konten baru, promo. Sederhana dan tidak butuh basis data token.
- Token untuk personal: "seseorang membalas komentarmu", "pesananmu dikirim". Butuh menyimpan token per pengguna.
Cara 3: Cloud Functions
Ini yang dipakai untuk notifikasi otomatis — misalnya memberi tahu pemilik poll ketika ada yang memberi suara.
bash
# Sekali di folder proyek
firebase init functions
# Pilih JavaScript atau TypeScriptjavascript
// functions/index.js
const { onDocumentCreated } = require('firebase-functions/v2/firestore');
const { initializeApp } = require('firebase-admin/app');
const { getFirestore } = require('firebase-admin/firestore');
const { getMessaging } = require('firebase-admin/messaging');
initializeApp();
const db = getFirestore();
/**
* Memberi tahu pemilik poll setiap kali ada suara baru.
*/
exports.notifikasiSuaraBaru = onDocumentCreated(
'polls/{pollId}/votes/{userId}',
async (event) => {
const { pollId, userId } = event.params;
const pollSnap = await db.doc(`polls/${pollId}`).get();
if (!pollSnap.exists) return;
const poll = pollSnap.data();
// Jangan beri tahu kalau pemilik memberi suara pada polnya sendiri
if (poll.pemilikId === userId) return;
const pemilikSnap = await db.doc(`users/${poll.pemilikId}`).get();
const token = pemilikSnap.data()?.fcmToken;
if (!token) return;
const pemilihSnap = await db.doc(`users/${userId}`).get();
const namaPemilih = pemilihSnap.data()?.nama ?? 'Seseorang';
await getMessaging().send({
token,
notification: {
title: 'Suara baru!',
body: `${namaPemilih} memberi suara pada "${poll.judul}".`,
},
data: {
tipe: 'poll',
id: pollId,
// Wajib untuk Android agar ketukan membuka aplikasi
click_action: 'FLUTTER_NOTIFICATION_CLICK',
},
android: {
priority: 'high',
notification: {
channelId: 'notifikasi_penting',
},
},
apns: {
payload: {
aps: { sound: 'default', badge: 1 },
},
},
});
}
);
/**
* Siaran ke topik ketika poll unggulan baru dibuat.
*/
exports.notifikasiPollUnggulan = onDocumentCreated(
'polls/{pollId}',
async (event) => {
const poll = event.data?.data();
if (!poll?.unggulan) return;
await getMessaging().send({
topic: 'semua_pengguna',
notification: {
title: 'Polling unggulan baru',
body: poll.judul,
},
data: {
tipe: 'poll',
id: event.params.pollId,
click_action: 'FLUTTER_NOTIFICATION_CLICK',
},
});
}
);Menerapkannya:
bash
firebase deploy --only functionsCloud Functions butuh paket Blaze
Cloud Functions tidak tersedia di paket gratis Spark. Kamu harus meningkatkan ke Blaze (bayar sesuai pemakaian).
Kabar baiknya, Blaze punya kuota gratis yang cukup besar — 2 juta pemanggilan per bulan. Untuk aplikasi kecil, tagihannya biasanya nol. Tetapi pasang budget alert tetap wajib, karena kesalahan kode bisa menyebabkan loop pemanggilan yang mahal.
Loop klasik: fungsi yang menulis ke dokumen yang memicunya sendiri. Selalu pastikan ada syarat berhenti.
Notifikasi terjadwal lokal
Untuk pengingat yang tidak butuh server:
dart
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
Future<void> jadwalkanPengingat({
required int id,
required String judul,
required String isi,
required DateTime waktu,
}) async {
tz.initializeTimeZones();
await _lokal.zonedSchedule(
id,
judul,
isi,
tz.TZDateTime.from(waktu, tz.local),
const NotificationDetails(
android: AndroidNotificationDetails(
'pengingat',
'Pengingat',
importance: Importance.high,
),
iOS: DarwinNotificationDetails(),
),
androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
uiLocalNotificationDateInterpretation:
UILocalNotificationDateInterpretation.absoluteTime,
);
}
Future<void> batalkanPengingat(int id) => _lokal.cancel(id);Android 12 ke atas butuh izin tambahan untuk alarm presisi:
xml
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />Memecahkan masalah
Push notification punya banyak titik gagal. Ini yang paling sering:
Notifikasi tidak sampai sama sekali.
- Izin sudah diberikan? Periksa authorizationStatus
- Token benar-benar tersimpan di Firestore?
- Untuk iOS: APNs key sudah diunggah ke Firebase?
- Untuk iOS: diuji di perangkat FISIK, bukan simulator?
- Perangkat punya koneksi internet?
- Google Play Services terpasang (Android)?
Sampai saat latar belakang, tidak sampai saat terbuka.
Ini perilaku normal. Android tidak menampilkan notifikasi otomatis saat aplikasi terbuka — kamu harus menampilkannya lewat flutter_local_notifications seperti pada _pasangPendengar() di atas.
Ikon notifikasi tampil sebagai kotak putih.
Ikonnya harus siluet putih dengan latar transparan.
Ketukan tidak membuka halaman yang benar.
Pastikan ketiga jalur ditangani: onMessage (terbuka), onMessageOpenedApp (latar belakang), dan getInitialMessage (tertutup). Melewatkan yang ketiga adalah kesalahan paling umum.
Token berubah dan notifikasi berhenti sampai.
Token berubah ketika aplikasi dipasang ulang, data dihapus, atau dipulihkan ke perangkat baru. Dengarkan onTokenRefresh dan perbarui Firestore.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Tiga Keadaan Aplikasi — ⭐⭐ · 45–60 menit
Tantangan: Pasang FCM di aplikasi mana pun yang sudah punya autentikasi, lalu buktikan ketiga keadaan bekerja: aplikasi sedang terbuka, sedang di latar belakang, dan tertutup sepenuhnya. Mengetuk notifikasi harus membuka layar yang tepat di ketiga kasus itu.
Bayangan tampilan (tiga keadaan):
text
Aplikasi terbuka Latar belakang Tertutup total
┌────────────────┐ ┌─────────────────┐ (aplikasi belum dibuka)
│ 🔔 Promo Baru! │ │ ┌─────────────┐ │ │
│ (ditampilkan │ │ │🔔 Promo Baru!│ │ tap ikon aplikasi
│ manual lewat │ │ │Diskon 50% │ │ │
│ local_notif) │ │ └─────────────┘ │ ▼
└────────────────┘ └─────────────────┘ langsung ke Layar Promo
onMessage onMessageOpenedApp getInitialMessage()Kriteria selesai:
onMessage,onMessageOpenedApp, dangetInitialMessage()ketiganya ditangani. Kamu sudah menguji yang ketiga dengan benar-benar menutup paksa aplikasi lebih dulu.- Saat aplikasi terbuka di Android, notifikasi tetap terlihat — karena kamu menampilkannya sendiri lewat
flutter_local_notifications. - Handler latar belakang adalah fungsi tingkat atas dengan
@pragma('vm:entry-point'). - Token disimpan ke dokumen pengguna, diperbarui lewat
onTokenRefresh, dan dihapus saat keluar akun. - Ikon notifikasi Android tampil sebagai siluet yang benar, bukan kotak putih.
Petunjuk: getInitialMessage() adalah yang paling sering terlewat karena ia satu-satunya yang tidak bisa kamu uji dengan aplikasi yang masih hidup di latar belakang — kamu harus benar-benar menutup paksa dulu. Ia mengembalikan pesan yang membuka aplikasi, atau null kalau aplikasi dibuka biasa, dan ia hanya bisa dipanggil sekali. Untuk kriteria token yang dihapus saat keluar akun: bayangkan ponsel bekas yang dijual. Kalau tokennya tetap tertaut ke akun lama, pemilik baru akan menerima notifikasi pribadi orang sebelumnya. Dan soal ikon kotak putih: Android mengabaikan warna pada ikon notifikasi dan hanya memakai bentuk alfanya — ikon berwarna penuh menjadi kotak solid.
Variasi 2: Peringatan Berbasis Topik — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun sistem peringatan cuaca ekstrem berbasis topik: pengguna berlangganan wilayah yang diminatinya, dan bisa berhenti berlangganan kapan saja. Tambahkan layar yang menampilkan topik apa saja yang sedang aktif.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ Wilayah Berlangganan: │
│ ☑ Jakarta │
│ ☑ Bandung │
│ ☐ Surabaya │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Notifikasi yang tiba menyebut wilayahnya dengan benar:
text
┌─────────────────────────────────┐
│ 🔔 Cuaca ekstrem di Jakarta │
│ Suhu mencapai 39°C │
└─────────────────────────────────┘Kriteria selesai:
- Berlangganan dan berhenti berlangganan topik bekerja, dan pilihannya bertahan setelah aplikasi dipasang ulang di perangkat yang sama.
- Kamu bisa menjelaskan kapan topik lebih tepat daripada token, dan sebaliknya.
- Keluar akun menghentikan seluruh langganan topik pengguna itu.
- Notifikasi topik yang tiba menampilkan wilayah asalnya dengan benar.
Petunjuk: Topik dan token menyelesaikan masalah yang berbeda: topik bagus untuk "semua orang yang peduli hal X" tanpa server perlu menyimpan daftar siapa pun, sementara token wajib untuk pesan yang ditujukan ke satu orang tertentu. Kriteria ketiga penting karena langganan topik menempel pada perangkat, bukan pada akun — pengguna berikutnya di perangkat itu akan terus menerima peringatan wilayah orang sebelumnya kalau kamu tidak membersihkannya.
Variasi 3: Pengingat Minum Obat — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun pengingat obat yang seluruhnya memakai notifikasi lokal terjadwal, tanpa server sama sekali: beberapa jadwal per hari, berulang setiap hari, dengan tombol "sudah diminum" dan "tunda" langsung di notifikasinya.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ 💊 Waktunya Minum Obat │
│ Vitamin C — 08:00 │
│ [ Sudah Diminum ] [Tunda 10m] │
└─────────────────────────────────┘Kedua tombol di atas bekerja tanpa membuka aplikasi sama sekali.
Kriteria selesai:
- Jadwal bertahan setelah perangkat dimulai ulang.
- Zona waktu ditangani dengan benar, termasuk ketika pengguna berpindah zona.
- Aksi pada notifikasi berfungsi tanpa membuka aplikasi.
- Android 13 ke atas meminta izin notifikasi dengan penjelasan lebih dulu.
Petunjuk: Notifikasi terjadwal hilang setelah perangkat dimulai ulang kecuali kamu mendaftarkan penerima yang menjadwalkannya kembali — di Android ini butuh izin dan deklarasi tambahan di manifest. Soal zona waktu: menjadwalkan pada "pukul delapan" memakai waktu absolut akan meleset ketika pengguna terbang ke zona lain; pustaka notifikasi lokal punya varian penjadwalan yang sadar zona waktu, dan itu yang kamu butuhkan.
Variasi 4: Tujuan dari Notifikasi — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Bangun penanganan navigasi dari notifikasi yang benar untuk lima jenis tujuan berbeda, termasuk layar yang biasanya butuh data dari layar induknya. Tangani juga notifikasi yang tiba ketika pengguna sedang berada di layar tujuannya.
Bayangan alur layar:
text
Notif "Pesan Baru dari Budi" ──▶ [Layar Chat #Budi]
...kalau Layar Chat #Budi SUDAH terbuka saat notif tiba:
isinya diperbarui di tempat, TIDAK menumpuk layar kedua yang sama
Notif "Promo Diskon" ──▶ [Detail Promo]
(dibuka langsung, tanpa lewat Daftar Promo — tetap memuat
datanya sendiri)Kriteria selesai:
- Kelima tujuan bekerja di ketiga keadaan aplikasi.
- Layar yang dibuka langsung dari notifikasi memuat datanya sendiri, tanpa bergantung pada layar induk.
- Notifikasi yang menunjuk layar yang sedang terbuka memperbarui isinya alih-alih menumpuk layar kedua yang sama.
- Payload notifikasi divalidasi, dan payload rusak tidak membuat aplikasi berhenti.
Petunjuk: Kriteria ketiga adalah bug yang membuat pengguna menekan tombol kembali lima kali untuk keluar: tanpa pemeriksaan, setiap notifikasi mendorong layar baru di atas layar yang identik. Periksa rute yang sedang aktif sebelum mendorong. Untuk kriteria keempat, payload datang dari server dan bisa saja berubah bentuk setelah pembaruan backend — perlakukan ia seperti JSON dari sumber luar mana pun, dengan nilai cadangan di setiap pembacaan.
Variasi 5: Diagnostik Notifikasi — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Bangun layar diagnostik internal yang menjawab pertanyaan "kenapa notifikasi tidak muncul": status izin, token saat ini, topik yang tertaut, apakah channel Android sudah terdaftar, dan riwayat pesan yang diterima aplikasi.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ Izin Notifikasi ✓ Diberikan │
│ Token: fXy8...m2Qp [Salin] │
│ Topik Aktif: jakarta, bandung │
│ Channel Android ✓ Terdaftar │
│ ┌─────────────────────────────┐ │
│ │ 09:15 Promo Baru masuk │ │
│ │ 08:00 Cuaca Ekstrem masuk │ │
│ └─────────────────────────────┘ │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Kriteria selesai:
- Layar menampilkan token yang bisa disalin, untuk diuji dari konsol Firebase.
- Status izin notifikasi ditampilkan dan disegarkan saat aplikasi kembali aktif.
- Riwayat pesan yang diterima disimpan lokal, termasuk yang diterima di latar belakang.
- Ada daftar periksa yang menunjukkan konfigurasi mana yang belum lengkap.
Petunjuk: Kriteria ketiga menyimpan tantangan yang menarik: handler latar belakang berjalan di isolate terpisah dan tidak berbagi memori dengan aplikasi utama, jadi menyimpan ke variabel tidak akan terlihat. Ia harus menulis ke penyimpanan yang bisa dibaca kedua sisi. Layar seperti ini terlihat berlebihan sampai kamu menghabiskan satu malam menebak kenapa notifikasi tidak sampai di satu perangkat tertentu — dan pada saat itu ia menghemat berjam-jam.
Ikhtisar
- FCM mengirim lewat token (perangkat tertentu) atau topik (sekelompok pengguna).
- Simpan token ke Firestore dan dengarkan
onTokenRefresh— token bisa berubah kapan saja. - Hapus token saat pengguna keluar akun, atau pengguna berikutnya di perangkat itu menerima notifikasi orang lain.
- Ada tiga keadaan yang harus ditangani:
onMessage(terbuka),onMessageOpenedApp(latar belakang), dangetInitialMessage()(tertutup). Melewatkan yang ketiga adalah kesalahan paling umum. - Android tidak menampilkan notifikasi otomatis saat aplikasi terbuka — tampilkan sendiri lewat
flutter_local_notifications. - Handler latar belakang wajib berupa fungsi tingkat atas dengan
@pragma('vm:entry-point'), dan berjalan di isolate terpisah tanpa akses state aplikasi. - Android 8+ butuh notification channel terdaftar; Android 13+ butuh izin runtime
POST_NOTIFICATIONS. - Ikon notifikasi Android harus siluet putih transparan, atau tampil sebagai kotak putih.
- iOS butuh APNs key, akun developer berbayar, dan perangkat fisik — tidak berfungsi di simulator.
- Pakai
navigatorKeyuntuk navigasi dari penanganan notifikasi, karena tidak adaBuildContextyang valid di sana. - Cloud Functions butuh paket Blaze. Pasang budget alert, dan pastikan fungsi tidak memicu dirinya sendiri.
- Sertakan
click_action: 'FLUTTER_NOTIFICATION_CLICK'di payload agar ketukan membuka aplikasi di Android.
Berikutnya: Bab 31 — Offline-First: SQFlite & Sinkronisasi.
Transkrip asli
Disintesis dari 1_flutter_core/13_push-notification.md (video 26–30: setup, requesting permissions & address token, testing, notification topics, sending via Cloud Functions) dan 4_flutter_ai-chatbot-n-firebase/6_firebase-cloud-messaging-and-apk-generation.md (FCM Part I & II). Lihat PDF Flutter Core dan PDF Firebase & AI.