Sejak Android 6.0 (API 23), permission berisiko tinggi seperti kamera, lokasi, dan mikrofon tidak lagi otomatis diberikan saat instalasi. Pengguna harus menyetujui setiap permission saat aplikasi pertama kali membutuhkannya. Pola ini disebut runtime permissions dan harus ditangani dengan kode — deklarasi di AndroidManifest.xml saja tidak cukup. Bab ini menjelaskan API requestPermission(), integrasi Compose lewat rememberLauncherForActivityResult(), serta kapan harus menampilkan UI rationale.

Tipe Permission #

Permission dibagi menjadi empat kategori berdasarkan tingkat risiko:

KategoriContohCara meminta
NormalINTERNET, VIBRATEOtomatis diberikan saat install
DangerousCAMERA, ACCESS_FINE_LOCATIONMinta saat runtime
SignatureBIND_DEVICE_ADMINHanya untuk aplikasi dengan signature yang sama dengan sistem
SpecialSYSTEM_ALERT_WINDOW, WRITE_SETTINGSMinta lewat Settings intent

Permission dangerous wajib diminta saat runtime. Untuk Android 13 (API 33), POST_NOTIFICATIONS juga termasuk dangerous sehingga butuh permintaan eksplisit.

Deklarasi di Manifest #

Meskipun permission dangerous diminta saat runtime, tetap harus dideklarasikan di AndroidManifest.xml:

xml AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

Tanpa deklarasi manifest, requestPermission akan langsung mengembalikan DENIED tanpa menampilkan dialog.

rememberLauncherForActivityResult di Compose #

Compose tidak memakai ActivityCompat.requestPermission secara langsung. Sebagai gantinya, kita memakai rememberLauncherForActivityResult dengan kontrak RequestPermission():

kotlin Compose permission launcher
@Composable
fun CameraScreen() {
    val context = LocalContext.current
    var hasCameraPermission by remember { mutableStateOf(
        ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
    ) }
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted -> hasCameraPermission = granted }

    if (!hasCameraPermission) {
        Button(onClick = { launcher.launch(Manifest.permission.CAMERA) }) {
            Text("Minta izin kamera")
        }
    } else {
        CameraPreview()
    }
}

Kontrak ActivityResultContracts.RequestPermission() menangani semua boilerplate: callback, code request, dan parsing hasil. Kita cukup menelepon launcher.launch(permission) dan menerima boolean hasil di lambda.

UI Rationale #

Saat pengguna menolak permission satu kali, permintaan kedua sebaiknya didahului penjelasan. Fungsi shouldShowRequestPermissionRationale() mengembalikan true bila:

  1. Permission pernah diminta dan ditolak (tidak “Never ask again”)
  2. Pengguna belum mencentang “Don’t ask again” pada dialog sistem
kotlin Pola rationale
if (ContextCompat.checkSelfPermission(context, permission) == GRANTED) {
    launchCamera()
} else if (shouldShowRequestPermissionRationale(activity, permission)) {
    showRationaleDialog(onConfirm = { launcher.launch(permission) })
} else {
    launcher.launch(permission)
}

Bila pengguna memilih “Never ask again” (atau “Don’t ask again” di Android 11+), shouldShowRequestPermissionRationale mengembalikan false. Pada kasus ini, kita perlu mengarahkan pengguna ke Settings aplikasi lewat intent:

kotlin Buka settings aplikasi
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
    data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)

Simulasi Permission Flow #

Cuplikan berikut memodelkan permission registry dan requester sebagai state machine. Permission yang sudah ditolak sebelumnya akan menampilkan rationale sebelum memanggil sistem:

kotlin
// permissions_runtime.kt
// Mensimulasikan runtime permission flow di Android 6+: cek status awal,
// tampilkan rationale bila sebelumnya ditolak, minta permission, dan tangani
// hasil (granted / denied). Tidak memerlukan Android SDK.

enum class PermissionState { GRANTED, DENIED, SHOW_RATIONALE, PERMANENTLY_DENIED }
enum class PermissionResult { GRANTED, DENIED }

class PermissionRegistry {
    private val granted = mutableSetOf<String>()
    private val askedBefore = mutableSetOf<String>()

    fun status(permission: String): PermissionState {
        return if (permission in granted) PermissionState.GRANTED
        else if (permission in askedBefore) PermissionState.SHOW_RATIONALE
        else PermissionState.DENIED
    }

    fun markAsked(permission: String) { askedBefore.add(permission) }
    fun grant(permission: String) { granted.add(permission); askedBefore.add(permission) }
    fun denyPermanently(permission: String) { askedBefore.add(permission) }
}

class PermissionRequester(private val registry: PermissionRegistry) {
    private val callbacks = mutableMapOf<String, (PermissionResult) -> Unit>()

    fun launch(permission: String, onResult: (PermissionResult) -> Unit) {
        callbacks[permission] = onResult
        when (registry.status(permission)) {
            PermissionState.GRANTED -> onResult(PermissionResult.GRANTED)
            PermissionState.SHOW_RATIONALE -> {
                println("  [Rationale] Tunjukkan alasan kenapa izin $permission diperlukan")
                askSystem(permission, onResult)
            }
            else -> askSystem(permission, onResult)
        }
        registry.markAsked(permission)
    }

    private fun askSystem(permission: String, onResult: (PermissionResult) -> Unit) {
        println("  [System] Menampilkan dialog permission untuk $permission")
    }

    // Simulasi callback dari sistem operasi
    fun simulateSystemResponse(permission: String, granted: Boolean) {
        if (granted) registry.grant(permission)
        else registry.denyPermanently(permission)
        callbacks[permission]?.invoke(if (granted) PermissionResult.GRANTED else PermissionResult.DENIED)
    }
}

fun main() {
    val registry = PermissionRegistry()
    val requester = PermissionRequester(registry)

    val permissions = listOf(
        "android.permission.CAMERA",
        "android.permission.ACCESS_FINE_LOCATION",
        "android.permission.POST_NOTIFICATIONS"
    )

    println("=== Status awal ===")
    permissions.forEach { println("  $it: ${registry.status(it)}") }

    println("\n=== Permintaan pertama untuk CAMERA (sistem menolak) ===")
    requester.launch("android.permission.CAMERA") { result ->
        println("  Callback: $result")
    }
    requester.simulateSystemResponse("android.permission.CAMERA", granted = false)

    println("\n=== Permintaan kedua untuk CAMERA (harus tampilkan rationale) ===")
    requester.launch("android.permission.CAMERA") { result ->
        println("  Callback: $result")
    }
    requester.simulateSystemResponse("android.permission.CAMERA", granted = true)

    println("\n=== Status akhir ===")
    permissions.forEach { println("  $it: ${registry.status(it)}") }
}

Output yang Dihasilkan:

text
=== Status awal ===
  android.permission.CAMERA: DENIED
  android.permission.ACCESS_FINE_LOCATION: DENIED
  android.permission.POST_NOTIFICATIONS: DENIED

=== Permintaan pertama untuk CAMERA (sistem menolak) ===
  [System] Menampilkan dialog permission untuk android.permission.CAMERA
  Callback: DENIED

=== Permintaan kedua untuk CAMERA (harus tampilkan rationale) ===
  [Rationale] Tunjukkan alasan kenapa izin android.permission.CAMERA diperlukan
  [System] Menampilkan dialog permission untuk android.permission.CAMERA
  Callback: GRANTED

=== Status akhir ===
  android.permission.CAMERA: GRANTED
  android.permission.ACCESS_FINE_LOCATION: DENIED
  android.permission.POST_NOTIFICATIONS: DENIED

Perhatikan bahwa permintaan kedua untuk CAMERA menampilkan rationale sebelum menampilkan dialog sistem — registry mengetahui bahwa permission sebelumnya ditolak. Permintaan ketiga (yang berhasil) langsung mengembalikan GRANTED karena status sudah GRANTED.

Referensi Lanjutan #

  • Permissions — panduan resmi sistem permission Android termasuk permission runtime sejak API 23

Selanjutnya #

Permission sering berhubungan dengan operasi latar belakang (mis. lokasi saat app tertutup). Bab Background Work dengan WorkManager akan membahas OneTimeWorkRequest, PeriodicWorkRequest, dan constraints.