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:
| Kategori | Contoh | Cara meminta |
|---|---|---|
| Normal | INTERNET, VIBRATE | Otomatis diberikan saat install |
| Dangerous | CAMERA, ACCESS_FINE_LOCATION | Minta saat runtime |
| Signature | BIND_DEVICE_ADMIN | Hanya untuk aplikasi dengan signature yang sama dengan sistem |
| Special | SYSTEM_ALERT_WINDOW, WRITE_SETTINGS | Minta 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:
<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():
@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:
- Permission pernah diminta dan ditolak (tidak “Never ask again”)
- Pengguna belum mencentang “Don’t ask again” pada dialog sistem
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:
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:
// 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:
=== 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: DENIEDPerhatikan 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.