Introduction
Jetpack Compose has revolutionized Android development by providing a modern, declarative approach to building user interfaces. However, even with this new paradigm, certain traditional Android tasks, such as handling runtime permissions, remain essential. Permissions are critical to maintaining user privacy and security, as they control access to sensitive data and device features like the camera, location, and contacts.
In this article, we’ll dive deep into handling permissions in Jetpack Compose, focusing on single and multiple permission scenarios using the `rememberPermissionState` and `rememberMultiplePermissionsState` functions from the Accompanist library.
Handling app permissions can definitely be a challenge, and the approach you take will depend heavily on your app’s specific needs. For instance, you might choose to request all necessary permissions upfront on the very first screen. Or, you might prefer to prompt the user for permissions only when a specific button is clicked. Another common approach is to handle permissions as the user navigates to different screens. Ultimately, the strategy you choose should align with your app’s flow and user experience.
This article doesn’t claim to cover every possible scenario, but by understanding the core concepts, you’ll be better equipped to tackle whatever permission challenges your app faces.
Please find the source code for this article below:
https://github.com/d-kostadinov/medium.handle.permission.git|
Check the sample preview on YouTube:
The Importance of Permission Handling in Android
Android applications often require access to features or data that could compromise user privacy if mishandled, such as the camera, microphone, or location services. Since Android 6.0 (API level 23), permissions are requested at runtime rather than during installation, allowing users to grant or deny permissions while the app is running. This shift empowers users but also requires developers to implement robust permission-handling logic to ensure their apps function correctly and securely.
In Jetpack Compose, while UI development has become more streamlined, handling permissions still requires careful attention.
This guide will walk you through managing both single and multiple permissions in a Compose-based Android application.
Initial Setup
Gradle dependency
Before we start lets add the Accompanist Permissions Dependency
Open your build.gradle
(usually the one at the module level, e.g., app/build.gradle
) and add the following dependency:
dependencies {
implementation "com.google.accompanist:accompanist-permissions:0.31.1-alpha"
}
You should be ready to go after a sync of the project.
Define permission in Manifest file
Before requesting any permission at runtime, you must declare it in the AndroidManifest.xml
file. This step is mandatory because Android checks if the app has declared all necessary permissions before it can request them from the user.
Here’s how you declare the camera and location permissions:
<!-- Declare Camera permission -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Declare Fine Location permission -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- Declare Coarse Location permission -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Declare that the app uses the camera hardware -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
Example 1: Handling Camera Permission in Jetpack Compose
Once you’ve declared the permission, you can now handle it dynamically within the app. This is where Accompanist’s rememberPermissionState
comes into play, allowing you to track and request the camera permission inside a composable function.
Here’s how you handle camera permission at runtime using Jetpack Compose:
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraPermissionHandlingScreen(navController: NavHostController) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
val context = LocalContext.current
// Track if the permission request has been processed after user interaction
var hasRequestedPermission by rememberSaveable { mutableStateOf(false) }
var permissionRequestCompleted by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(cameraPermissionState.status) {
// Check if the permission state has changed after the request
if (hasRequestedPermission) {
permissionRequestCompleted = true
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
when (val status = cameraPermissionState.status) {
is PermissionStatus.Granted -> {
// Permission granted, show success message
Text("Camera permission granted. You can now use the camera.")
Button(onClick = { navController.popBackStack() }, Modifier.padding(top = 16.dp)) {
Text("Go Back")
}
}
is PermissionStatus.Denied -> {
if (permissionRequestCompleted) {
// Show rationale only after the permission request is completed
if (status.shouldShowRationale) {
Text("Camera permission is required to use this feature.")
Button(onClick = {
cameraPermissionState.launchPermissionRequest()
hasRequestedPermission = true
}) {
Text("Request Camera Permission")
}
} else {
// Show "Denied" message only after the user has denied permission
Text("Camera permission denied. Please enable it in the app settings to proceed.")
Button(onClick = {
// Open app settings to manually enable the permission
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}) {
Text("Open App Settings")
}
}
} else {
// Show the initial request button
Button(onClick = {
cameraPermissionState.launchPermissionRequest()
hasRequestedPermission = true
}) {
Text("Request Camera Permission")
}
}
}
}
}
}
Deep Explanation
Let’s walk through this first example step by step. In this case, you’re handling only one permission — the camera permission — using the Accompanist Permissions API in Jetpack Compose. Here’s a deep explanation:
1. Permission State Management
In this example, you’re using rememberPermissionState
to track the status of a single permission (the camera permission):
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
rememberPermissionState is a Composable function that manages the status of the requested permission (
Manifest.permission.CAMERA
). It returns the current status of the camera permission, which could either be granted or denied.
2. Handling Permission Status with when
Expression
You use a when
expression to manage what to display in the UI based on the permission status:
when (val status = cameraPermissionState.status) {
is PermissionStatus.Granted -> {
// Permission is granted
}
is PermissionStatus.Denied -> {
// Permission is denied or rationale is needed
}
}
Here, two main states are handled:
- Permission Granted (
PermissionStatus.Granted): This block runs when the camera permission has been granted. A simple message confirms that the user can now use the camera, and a button is displayed to allow navigation back to the previous screen.
- Permission Denied (
PermissionStatus.Denied): If the permission is denied, you show different UI elements depending on whether the user needs a rationale for the permission or whether they have fully denied the permission.
3. Tracking Permission Requests
You maintain state variables to track whether the permission request has been triggered by the user, and whether the request has completed:
var hasRequestedPermission by rememberSaveable { mutableStateOf(false) }
var permissionRequestCompleted by rememberSaveable { mutableStateOf(false) }
hasRequestedPermission: This variable is used to ensure that the permission request is only tracked after the user has interacted with the permission dialog.
permissionRequestCompleted: This tracks whether the permission request has been processed, ensuring you don’t show a denial message prematurely.
These states are updated when the permission status changes:
LaunchedEffect(cameraPermissionState.status) {
if (hasRequestedPermission) {
permissionRequestCompleted = true
}
}
This LaunchedEffect
block ensures that once the user has interacted with the permission dialog, the permissionRequestCompleted
flag is set to true
. This helps manage what message or action to display in the UI after the request is processed.
4. Handling Denial and Providing Rationale
When the permission is denied, the PermissionStatus.Denied
block is responsible for showing the appropriate message to the user based on whether a rationale should be shown:
status.shouldShowRationale: This flag indicates whether the system recommends showing an explanation to the user about why the app needs the permission (usually when the user denies the permission the first time). If the rationale needs to be shown, you prompt the user to request the permission again:
if (status.shouldShowRationale) {
Text("Camera permission is required to use this feature.")
Button(onClick = {
cameraPermissionState.launchPermissionRequest()
hasRequestedPermission = true
}) {
Text("Request Camera Permission")
}
}
- No Rationale Needed (Denied without
shouldShowRationale): This occurs if the user has denied the permission and selected the “Don’t ask again” option, or if the permission request has been denied multiple times. In this case, the app directs the user to the app’s settings page to manually enable the permission:
Text("Camera permission denied. Please enable it in the app settings to proceed.")
Button(onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}) {
Text("Open App Settings")
}
This code launches an intent to the app settings, allowing the user to manually grant the camera permission.
5. Initial Permission Request UI
Before the user interacts with the permission dialog for the first time, the UI presents a button to request the camera permission:
Button(onClick = {
cameraPermissionState.launchPermissionRequest()
hasRequestedPermission = true
}) {
Text("Request Camera Permission")
}
- The
launchPermissionRequest()
function launches the system permission dialog where the user can either grant or deny the permission. hasRequestedPermission = true
ensures that the request is being tracked once the button is clicked.
Job Offers
6. Handling Success
When the user grants the camera permission, the app shows a success message and provides a button to navigate back:
Text("Camera permission granted. You can now use the camera.")
Button(onClick = { navController.popBackStack() }, Modifier.padding(top = 16.dp)) {
Text("Go Back")
}
This simple feedback reassures the user that they can now access the camera, and gives them an option to navigate away from the permission request screen.
Summary of Key Points:
- State-driven UI: The UI changes dynamically based on whether the permission is granted, denied, or needs rationale.
- Single permission handling: This example simplifies the logic compared to multiple permissions, focusing on only the camera permission.
- State tracking: The app tracks whether the permission request has been initiated and completed, ensuring a smooth user experience.
- Handling user denials: The app either shows a rationale when necessary or directs users to the app settings if the permission has been permanently denied.
- Settings redirection: In the case of a permanent denial (e.g., “Don’t ask again”), the app directs the user to the system settings to manually enable the permission.
This is a clean and straightforward implementation for managing a single permission request in Jetpack Compose. It handles the full lifecycle, from the initial request to handling denials and offering redirection to the app settings, providing a user-friendly experience.
Example 2: Handling Both Camera and Location Permissions in Jetpack Compose
In many cases, apps need to request multiple permissions simultaneously. For instance, a photo-sharing app might need both camera and location access. This scenario can be handled using rememberMultiplePermissionsState
.
Source code
Here’s how you can handle both camera and location permissions in Jetpack Compose using rememberMultiplePermissionsState
:
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberMultiplePermissionsState
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CameraAndLocationPermissionsHandlingScreen(navController: NavHostController) {
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION
)
)
val context = LocalContext.current
// State to track if the permission request has been processed
var hasRequestedPermissions by rememberSaveable { mutableStateOf(false) }
var permissionRequestCompleted by rememberSaveable { mutableStateOf(false) }
// Update permissionRequestCompleted only after the user interacts with the permission dialog
LaunchedEffect(permissionsState.revokedPermissions) {
if (hasRequestedPermissions) {
permissionRequestCompleted = true
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
when {
permissionsState.allPermissionsGranted -> {
// If all permissions are granted, show success message
Text("All permissions granted. You can now access the camera and location.")
Button(onClick = { navController.popBackStack() }, Modifier.padding(top = 16.dp)) {
Text("Go Back")
}
}
permissionsState.shouldShowRationale -> {
// Show rationale if needed and give an option to request permissions
Text("Camera and Location permissions are required to use this feature.")
Button(onClick = {
permissionsState.launchMultiplePermissionRequest()
}) {
Text("Request Permissions")
}
}
else -> {
if (permissionRequestCompleted) {
// Show permission denied message only after interaction
Text("Permissions denied. Please enable them in the app settings to proceed.")
Button(onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}) {
Text("Open App Settings")
}
} else {
// Display the initial request button
Text("Camera and Location permissions are required to use this feature.")
Button(onClick = {
permissionsState.launchMultiplePermissionRequest()
hasRequestedPermissions = true
}) {
Text("Request Permissions")
}
}
}
}
}
}
Deep Explanation
This code demonstrates how to handle multiple permissions (in this case, camera and location) in Jetpack Compose using Accompanist Permissions
API. Here’s a deep dive into how it works:
1. Permissions State Management
The key part of this implementation revolves around rememberMultiplePermissionsState
. This state is used to track and manage multiple permissions, specifically for the camera and fine location, as specified by:
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION
)
)
rememberMultiplePermissionsState
is aComposable
function that keeps track of the permission statuses for each requested permission. It returns an object containing the status of each permission, whether granted, denied, or requiring a rationale.
2. Handling Permission Request States
You’re handling different states within the permission request process using a when
expression:
when {
permissionsState.allPermissionsGranted -> {
// Handle the case where all permissions are granted
}
permissionsState.shouldShowRationale -> {
// Show rationale to the user
}
else -> {
// Handle permission denial or the initial request
}
}
Each state represents a different point in the permission lifecycle:
- All permissions granted (
permissionsState.allPermissionsGranted): This block is executed when both permissions (camera and location) are granted. It shows a success message and provides a “Go Back” button to navigate away from the screen.
- Rationale Needed (
permissionsState.shouldShowRationale): This state is active when permissions have been denied before but the user has not checked the “Don’t ask again” option. It allows the app to explain why these permissions are needed. Here, you provide an explanation and request the permissions again with a button click.
- Permissions Denied: If permissions are denied or if the user needs to enable permissions from settings, a separate message is displayed. This block handles the flow after the user has interacted with the permission dialog.
3. Tracking Permission Requests
You maintain a state variable hasRequestedPermissions
to track whether the user has already triggered the permission request:
var hasRequestedPermissions by rememberSaveable { mutableStateOf(false) }
- This helps in managing whether the permission dialog has been presented to the user. After launching the permission request, you set
hasRequestedPermissions
totrue
:
Button(onClick = {
permissionsState.launchMultiplePermissionRequest()
hasRequestedPermissions = true
}) {
Text("Request Permissions")
}
4. Handling User Denials and Settings Redirection
Once the permission request is completed, and the user has denied permissions, the code shows a message that informs them of the need to enable permissions manually from the app settings:
Text("Permissions denied. Please enable them in the app settings to proceed.")
Button(onClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
}) {
Text("Open App Settings")
}
- This button opens the app’s settings page where the user can manually enable the denied permissions. This is crucial for handling cases where the user chooses “Don’t ask again” when denying permissions.
5. Handling Permission Dialog Interaction
You also handle the lifecycle of the permission request dialog using LaunchedEffect
:
LaunchedEffect(permissionsState.revokedPermissions) {
if (hasRequestedPermissions) {
permissionRequestCompleted = true
}
}
LaunchedEffect
is triggered whenever the list of revoked permissions changes. This ensures thatpermissionRequestCompleted
is set totrue
only after the user interacts with the permission dialog. This helps to differentiate between an initial state and after the user has made a decision (denied or granted the permissions).
Summary of Key Points:
- State-driven UI: The UI updates dynamically based on the permission status (granted, denied, or rationale required).
- State management with
rememberSaveable: Used to track whether the permission request has already been initiated, persisting the state across configuration changes.
- User-friendly permissions handling: Explains the need for permissions with a rationale, manages user denials, and redirects the user to app settings if needed.
- Experimental API: Uses
Accompanist Permissions
, an experimental API to handle permissions cleanly in Jetpack Compose.
This approach offers a seamless user experience while managing multiple permissions with Jetpack Compose, taking into account various scenarios like denials, rationale, and settings redirection.
Conclusion
Handling permissions in Jetpack Compose requires a thoughtful approach to ensure both security and usability. By leveraging tools like `rememberPermissionState` and `rememberMultiplePermissionsState`, you can seamlessly integrate permission management into your Compose-based UI, providing a cohesive and responsive user experience. Whether your app needs access to a single feature like the camera or multiple features like the camera and location, these techniques will help you handle permissions efficiently and effectively.
This article is previously published on proandroiddev.com