How to Request Android Runtime Permissions using Jetpack Compose?
A simple app example and proper way to request Android runtime permission workflow using Accompanist Permissions library for Jetpack Compose
There are 2 types of permissions:
Install-time permissions
Runtime permissions
An example of the most common install-time permissions is internet permissions. The Android OS system automatically grants your app the permission when the app is installed. You just need to declare the permission in the AndroidManifest.xml.
Unlike runtime permissions, in addition to declaring the permissions in the AndroidManifest.xml, you also need to manually request the permissions in your app. There are some gotchas on requesting runtime permissions, which will be discussed later.
Why Accompanist Permissions?
After playing around with different Android permission implementations in Jetpack Compose, I prefer to use Accompanist permissions library over rememberLauncherForActivityResult()
for the following reasons:
Permissions state checking is slightly easier using
rememberPermissionState()
orrememberMultiplePermissionsState()
instead of waiting for callback results from the activity result launcher.Accessing to
shouldShowRequestPermissionRationale()
in composable function is not that straight forward. You probably want to override theshouldShowRequestPermissionRationale()
inActivity
and do something like this, and figure out how this can be accessed from your composable function.class MainActivity : ComponentActivity() { /*...*/ override fun shouldShowRequestPermissionRationale( permission: String) : Boolean { return if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { super.shouldShowRequestPermissionRationale(permission) } else { false } } }
In Accompanist permission, you can just access the
PermissionStatus.shouldShowRationale
variable fromPermissionState.
Request Runtime Permission Gotchas
Permission Request Dialog is Dismissible
Request runtime permission allows user to dismiss the permission dialog in certain Android version (I think is starting from Android 11/ API level 30). You can either click outside the permission request dialog or simply press the back button to dismiss the permission request dialog.
The problem is, when the permission dialog is dismissed, the PermissionState
data remains unchanged. So we have no idea, the user has dismissed the permission request dialog.
What I did to workaround with this issue is I launch the permission request on top of another OptionalLaunchPermissionDialog()
which is the wrapper for AlertDialog().
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalSinglePermissionScreen(
permission: String,
) {
/*...*/
OptionalLaunchPermissionDialog(
permission,
permissionState,
dismissCallback = { launchPermissionDialog = false}
)
SideEffect {
permissionState.launchPermissionRequest()
}
}
So, when the permission request is dismissed, the AlertDialog()
is shown to allow user to relaunch or cancel the permission request (if the permission request is optional).
Don't Launch Permission Request when shouldShowRationale is True
When shouldShowRationale
is True, it means the permission has been denied before. The launchPermissionRequest()
was first called before. If you call launchPermissionRequest()
the second time, it still works. However, if you deny the permission this time, the shouldShowRationale
is now set to false. You call launchPermissionRequest()
, nothing will happen.
So after this stage, you completely lost the permission state status. You do not know the runtime permission request has been permanently denied. So, the best practice here in my opinion is to show the rational UI to guide user to enable the permission manually when shouldShowRationale
is set to True.
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalSinglePermissionScreen(
permission: String,
) {
val permissionState = rememberPermissionState(permission)
if (permissionState.status.isGranted) {
// permission granted - do nothing here
} else if (permissionState.status.shouldShowRationale) {
// permission denied - show rational UI here
} else {
// launch runtime permission request here
}
}
Request Runtime Permission App Demo
This demo app has the following examples:
Optional Single Runtime Permission Request
Required Single Runtime Permission Request
Optional Multiple Runtime Permissions Request
Required Multiple Runtime Permissions Request
The step-by-step guide here refers to Optional Single Runtime Permission Request. For the rest, you can refer to the source code.
1. Declare Permission in Your Manifest File
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CALL_PHONE" />
<!-- ... -->
</manifest>
2. Add Accompanist Permissions Dependency
dependencies {
/*...*/
implementation ("com.google.accompanist:accompanist-permissions:0.31.0-alpha")
}
3. Implement Permission Request Logic
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalSinglePermissionScreen(
permission: String,
) {
var permissionStatusText by remember { mutableStateOf("") }
val permissionState = rememberPermissionState(permission)
var launchPermissionDialog by remember { mutableStateOf(true) }
var showRationale by remember { mutableStateOf(true) }
if (permissionState.status.isGranted) {
permissionStatusText = "Granted"
}
else if (permissionState.status.shouldShowRationale) {
permissionStatusText = "Denied"
if(showRationale) {
OptionalRationalPermissionDialog(
permission,
dismissCallback = {showRationale = false}
)
}
} else {
permissionStatusText = "N/A"
if (launchPermissionDialog) {
OptionalLaunchPermissionDialog(
permission,
permissionState,
dismissCallback = { launchPermissionDialog = false}
)
SideEffect {
permissionState.launchPermissionRequest()
}
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text("Optional Permission status: $permissionStatusText")
}
}
This is the OptionalLaunchPermissionDialog()
that allows you to relaunch the permission request when the permission request dialog is dismissed.
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalLaunchPermissionDialog (
permission: String,
permissionState: PermissionState,
dismissCallback: () -> Unit
) {
val context = LocalContext.current
val permissionLabel = stringResource(
context.packageManager.getPermissionInfo(permission, 0).labelRes
)
AlertDialog(
onDismissRequest = { dismissCallback()},
title = { Text(text = "Permission Required!") },
text = { Text(text = permissionLabel) },
confirmButton = {
Button(onClick = {
permissionState.launchPermissionRequest()
}) {
Text(text = "Launch")
}
},
dismissButton = {
Button(onClick = {
dismissCallback()
}) {
Text(text = "Cancel")
}
}
)
}
This
stringResource(context.packageManager.getPermissionInfo(permission, 0).labelRes)
provides a more user-friendly permission string.Since this is an optional permission request, we allow the user to dismiss this dialog.
OptionalRationalPermissionDialog()
shows the reasons why the app needs the permission and guide the user to grant the permission manually.
@Composable
fun OptionalRationalPermissionDialog (
permission: String,
dismissCallback: () -> Unit
) {
val context = LocalContext.current
val permissionLabel = stringResource(
context.packageManager.getPermissionInfo(permission, 0).labelRes
)
AlertDialog(
onDismissRequest = { dismissCallback()},
title = { Text(text = "Permission Required!") },
text = { Text(text = permissionLabel) },
confirmButton = {
Button(onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.apply {
data = Uri.fromParts("package", context.packageName, null)
}
ContextCompat.startActivity(context, intent, null)
}) {
Text(text = "Go to settings")
}
},
dismissButton = {
Button(onClick = {
dismissCallback()
}) {
Text(text = "Cancel")
}
}
)
}
This creates an activity intent that brings you to the app setting which you manually grant the permission to your app.
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.apply {
data = Uri.fromParts("package", context.packageName, null)
}
ContextCompat.startActivity(context, intent, null)
Well, that is it!
4. Revoke Runtime Permissions
For testing, we often want to revoke the runtime permissions and rerun the app again. So I gave this ADB command a try.
adb shell pm revoke vtsen.hashnode.dev.runtimepermissiondemoapp android.permission.CALL_PHONE
It does remove the permission, but it has an unexpected behavior. The shouldShowRationale
is now set to True. I want it to be False. To completely revoke the runtime permissions, I either need to reinstall the app or clear the app storage.
Conclusion
Multiple permissions request is similar. Instead of having single PermissionState
, it has List<PermissionState>.
I will probably use it by default in my app because it can also support single permission request. Last but not least, when shouldShowRationale
is True, do NOT launch the runtime permission request.
Source Code
GitHub Repository: Demo_RuntimePermission