How to Request Android Runtime Permissions using Jetpack Compose?
Tags: android, kotlin, beginners
Published: 2023-07-01 00:00:00

**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()` or `rememberMultiplePermissionsState()` 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 the `shouldShowRequestPermissionRationale()` in `Activity` and do something like this, and figure out how this can be accessed from your composable function.
    
    ```kotlin
    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 from `PermissionState.`
    

## 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().`

```kotlin
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalSinglePermissionScreen(
    permission: String,
) {
    /*...*/
    OptionalLaunchPermissionDialog(
        permission,
        permissionState,
        dismissCallback = { launchPermissionDialog = false}
    )

    SideEffect {
        permissionState.launchPermissionRequest()
    }        
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686455489862/d47ac938-6dbd-4a72-a361-f64f1076dcea.gif)

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.

```kotlin
@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

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <!-- ... -->
</manifest>
```

### 2\. Add Accompanist Permissions Dependency

```kotlin
dependencies {
    /*...*/
    implementation ("com.google.accompanist:accompanist-permissions:0.31.0-alpha")
}
```

### 3\. Implement Permission Request Logic

```kotlin
@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.

```kotlin
@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")
            }
        }
    )
}
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686467395444/2f888e0a-4d32-4fb5-9a3c-90ef952d6253.png)

* 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.

```kotlin
@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.

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1686467487957/ac2aeb5f-797a-4731-9ca0-1da0fba3eab3.png)

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.

```bash
 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](https://github.com/vinchamp77/Demo_RuntimePermission)

---

*Originally published at [https://vtsen.hashnode.dev](https://vtsen.hashnode.dev/how-to-request-android-runtime-permissions-using-jetpack-compose)*.