Notifikasi adalah cara utama aplikasi Android mengomunikasikan peristiwa ke pengguna di luar UI. Sejak Android 8.0 (API 26), setiap notifikasi wajib berasosiasi dengan notification channel — kategori yang dapat pengguna atur secara terpisah. Bab ini menjelaskan cara membuat channel, menampilkan notifikasi dasar, menambahkan action button lewat PendingIntent, dan menerbitkan notifikasi foreground service yang wajib untuk Service berjalan lama.

Notification Channels #

Channel adalah kategori yang dikenali pengguna. Pengguna dapat mematikan channel “Promosi” tanpa mematikan “Pesanan”. Channel dibuat sekali per aplikasi dan tidak dapat diubah setelah dibuat — yang dapat diubah hanya nama dan deskripsi, bukan importance.

kotlin Buat channel
val channel = NotificationChannel(
    "pesan",
    "Pesan Baru",
    NotificationManager.IMPORTANCE_HIGH  // 1=LOW, 2=DEFAULT, 3=HIGH
).apply {
    description = "Notifikasi pesan masuk"
    enableVibration(true)
    enableLights(true)
}

val nm = context.getSystemService(NotificationManager::class.java)
nm.createNotificationChannel(channel)

Tabel importance menentukan perilaku default channel:

ImportanceSuaraPopup heads-upStatus bar
IMPORTANCE_HIGHYaYaYa
IMPORTANCE_DEFAULTYaTidakYa
IMPORTANCE_LOWTidakTidakYa
IMPORTANCE_MINTidakTidakTidak (hanya shade)

Notification Dasar dengan PendingIntent #

Notifikasi dasar terdiri dari judul, teks, icon kecil, dan PendingIntent yang dipanggil saat pengguna mengetuk notifikasi. PendingIntent membungkus Intent sehingga sistem dapat mengeksekusinya atas nama aplikasi kita, bahkan jika aplikasi tidak berjalan.

kotlin Notifikasi dengan PendingIntent
val intent = Intent(context, TugasDetailActivity::class.java).apply {
    putExtra("TUGAS_ID", 42)
    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
    context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)

val notification = NotificationCompat.Builder(context, "tugas")
    .setSmallIcon(R.drawable.ic_notif)
    .setContentTitle("Pengingat: Belajar Kotlin")
    .setContentText("Tugas id=42 belum selesai")
    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
    .setContentIntent(pendingIntent)
    .setAutoCancel(true)
    .build()

NotificationManagerCompat.from(context).notify(1001, notification)

setAutoCancel(true) membuat notifikasi hilang saat diketuk. FLAG_IMMUTABLE wajib di Android 12+ untuk alasan keamanan.

Action Button #

Action button memungkinkan pengguna merespons notifikasi tanpa membuka aplikasi. Setiap action memerlukan PendingIntent sendiri (biasanya ke BroadcastReceiver).

kotlin Tambahkan action
val selesaiIntent = Intent(context, TugasActionReceiver::class.java).apply {
    action = "ACTION_SELESAI"
    putExtra("TUGAS_ID", 42)
}
val selesaiPending = PendingIntent.getBroadcast(
    context, 0, selesaiIntent, PendingIntent.FLAG_IMMUTABLE
)

val notif = NotificationCompat.Builder(context, "tugas")
    .setSmallIcon(R.drawable.ic_notif)
    .setContentTitle("Pengingat Tugas")
    .setContentText("Belajar Kotlin")
    .addAction(R.drawable.ic_check, "Tandai Selesai", selesaiPending)
    .build()

Foreground Service Notification #

Service yang berjalan di foreground (untuk tugas yang harus tetap aktif saat pengguna keluar, mis. pemutar musik atau tracking GPS) wajib menampilkan notifikasi persistent. Tanpa notifikasi, sistem akan menghentikan Service.

kotlin Start foreground service
class PlayerService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notif = NotificationCompat.Builder(this, "musik")
            .setContentTitle("Memutar musik")
            .setContentText("Lagu: Belajar Kotlin")
            .setSmallIcon(R.drawable.ic_play)
            .build()
        startForeground(NOTIFICATION_ID, notif)  // wajib untuk foreground service
        return START_STICKY
    }
}

Permission FOREGROUND_SERVICE wajib dideklarasikan di manifest. Mulai Android 14 (API 34), setiap tipe foreground service (mis. mediaPlayback, location, dataSync) butuh permission spesifik.

Simulasi NotificationManager #

Cuplikan berikut memodelkan channel, notifikasi, dan PendingIntent sebagai struktur data ringkas. NotificationManager memvalidasi bahwa channel sudah dibuat sebelum notifikasi diterbitkan:

kotlin
// notifications.kt
// Mensimulasikan notification channels (Android 8+), NotificationManager,
// PendingIntent, dan foreground service sebagai struktur data ringkas.

data class NotificationChannel(
    val id: String,
    val name: String,
    val importance: Int,  // 1=LOW, 2=DEFAULT, 3=HIGH
    val description: String
)

data class Notification(
    val id: Int,
    val channelId: String,
    val title: String,
    val text: String,
    val priority: Int,
    val pendingIntent: PendingIntent? = null,
    val actions: List<NotificationAction> = emptyList()
)

data class PendingIntent(val target: String, val extras: Map<String, Any?> = emptyMap())

data class NotificationAction(val title: String, val intent: PendingIntent)

class NotificationManager {
    private val channels = mutableMapOf<String, NotificationChannel>()
    private val notifications = mutableMapOf<Int, Notification>()

    fun createChannel(channel: NotificationChannel) {
        require(channels.put(channel.id, channel) == null) {
            "Channel ${channel.id} sudah ada"
        }
        println("  [Channel] dibuat: ${channel.id} (${channel.name}) importance=${channel.importance}")
    }

    fun notify(notification: Notification) {
        require(channels.containsKey(notification.channelId)) {
            "Channel ${notification.channelId} belum terdaftar"
        }
        notifications[notification.id] = notification
        println("  [Notify] id=${notification.id} channel=${notification.channelId}")
        println("    title=${notification.title} text=${notification.text}")
        notification.actions.forEach { println("    action: ${it.title} -> ${it.intent.target}") }
    }

    fun cancel(id: Int) {
        notifications.remove(id)
        println("  [Cancel] id=$id")
    }

    fun activeCount(): Int = notifications.size
    fun getChannel(id: String): NotificationChannel? = channels[id]
}

fun main() {
    val nm = NotificationManager()

    println("=== Buat notification channels (Android 8+ wajib) ===")
    nm.createChannel(NotificationChannel("pesan", "Pesan Baru", 3, "Notifikasi pesan masuk"))
    nm.createChannel(NotificationChannel("tugas", "Pengingat Tugas", 2, "Notifikasi pengingat tugas"))
    nm.createChannel(NotificationChannel("download", "Status Download", 1, "Progress download"))

    println("\n=== Notify dengan action button ===")
    val bukaIntent = PendingIntent("MainActivity", mapOf("tugas_id" to 42))
    val tandaiSelesai = PendingIntent("TugasActionReceiver", mapOf("action" to "selesai", "tugas_id" to 42))
    nm.notify(Notification(
        id = 1001,
        channelId = "tugas",
        title = "Pengingat: Belajar Kotlin",
        text = "Tugas id=42 belum selesai",
        priority = 2,
        pendingIntent = bukaIntent,
        actions = listOf(
            NotificationAction("Tandai Selesai", tandaiSelesai),
            NotificationAction("Tunda 1 Jam", PendingIntent("TugasActionReceiver", mapOf("action" to "tunda")))
        )
    ))

    println("\n=== Foreground service notification (wajib Android 8+) ===")
    nm.notify(Notification(
        id = 2001,
        channelId = "download",
        title = "Mengunduh file...",
        text = "Progress: 45%",
        priority = 1,
        pendingIntent = PendingIntent("DownloadActivity")
    ))

    println("\n=== Status akhir ===")
    println("Notifikasi aktif: ${nm.activeCount()}")
    nm.cancel(2001)
    println("Setelah cancel 2001: ${nm.activeCount()} aktif")
}

Output yang Dihasilkan:

text
=== Buat notification channels (Android 8+ wajib) ===
  [Channel] dibuat: pesan (Pesan Baru) importance=3
  [Channel] dibuat: tugas (Pengingat Tugas) importance=2
  [Channel] dibuat: download (Status Download) importance=1

=== Notify dengan action button ===
  [Notify] id=1001 channel=tugas
    title=Pengingat: Belajar Kotlin text=Tugas id=42 belum selesai
    action: Tandai Selesai -> TugasActionReceiver
    action: Tunda 1 Jam -> TugasActionReceiver

=== Foreground service notification (wajib Android 8+) ===
  [Notify] id=2001 channel=download
    title=Mengunduh file... text=Progress: 45%

=== Status akhir ===
Notifikasi aktif: 2
  [Cancel] id=2001
Setelah cancel 2001: 1 aktif

Perhatikan bahwa NotificationManager.notify() memvalidasi channel terlebih dahulu. Tanpa channel, panggilan akan gagal — persis seperti runtime Android yang menolak notifikasi tanpa channel.

Referensi Lanjutan #

  • Notifications — panduan resmi Notifications API Android untuk menampilkan notifikasi pengguna

Selanjutnya #

Notifikasi adalah satu aspek UX. Bab Android Security akan membahas aspek keamanan: EncryptedSharedPreferences, EncryptedFile, network security config, dan ProGuard/R8 untuk obfuscation.