Hello everyone! Today we will be talking about permission handling in android with jetpack compose.

We will discuss various use cases and permission handling scenarios in this article. By the end of it all, you will have everything with you to manage permissions in your android app.

The entire code discussed can be found in the repository given below.

Here is a little preview of what we will be building:

A simple registration page UI

If you want to use a library something like Accompanist for adding requesting permission, you can refer to my another article.
We will also discuss few use cases that you might encounter while handling permissions in the app after completing the implementation of permissions.

Dependencies

implementation "androidx.compose.ui:ui:1.2.0-beta02"
implementation "androidx.compose.material:material:1.2.0-beta02"

1. Handling runtime permissions.

There are various ways to deal with permissions. In this section, we will discuss managing runtime permissions without using an accompanist library.
So, let’s begin!

2. Declaring permissions.

Let us begin by declaring the permissions we use.
Since it is a cleaner way and a good practice to categorize and separate your code into files and packages, Let us create a permissions package and create a permissions file Permissions.kt to declare all the permissions we use in the entire app.

package com.example.permissionsapp.permissions

import android.Manifest

const val CAMERA_PERMISSION = Manifest.permission.CAMERA

Although we will be working with CAMERA_PERMISSION alone, we can also add multiple permissions like this.

package com.example.permissionsapp.permissions

import android.Manifest

const val CAMERA_PERMISSION = Manifest.permission.CAMERA
const val BATTERY_STATS = Manifest.permission.BATTERY_STATS
const val SET_TIME_ZONE = Manifest.permission.SET_TIME_ZONE

fun samplePermissions(): List<String> = listOf(
    BATTERY_STATS,
    SET_TIME_ZONE
)

3. Validating permissions

In the same package, let us also create a ValidatePermissions.kt to add our functions to validate our permissions.

package com.example.permissionsapp.permissions

import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat

fun isPermissionGranted(
    context: Context,
    permission: String
): Boolean {
    return ContextCompat.checkSelfPermission(
        context,
        permission
    ) == PackageManager.PERMISSION_GRANTED
}

Here, the isPermissionGranted() function uses context and permission as parameters and returns a Boolean.

Similarly, we can also configure this function to validate multiple permissions.

fun isPermissionGranted(
    context: Context,
    permission: List<String>
): Boolean {
    return permission.all { eachPermission ->
        ContextCompat.checkSelfPermission(
            context,
            eachPermission
        ) == PackageManager.PERMISSION_GRANTED
    }
}

Instead of a single permission, it requires a List of permissions to validate each one.

4. Creating permission launcher

Now, before building our UI, let us create a permission launcher, to use it in our UI.

@Composable
fun permissionLauncher(): ManagedActivityResultLauncher<String, Boolean> {

    return rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) {  }
}

This function prompts the RequestPermission prompt.

Permission dialog box prompted.

5. Building the UI

Now, let us create a composable ProfileScreen( ) which is our main screen.

@Composable
fun ProfileScreen() {
    val launcherForImageCapture = permissionLauncher()
}

Here we have used a variable launcherForImageCapture to call our permissionLauncher( )

Moving ahead, let us also create another composable ScreenContent( ) where we build our UI.

@Composable
private fun ScreenContent(
    modifier: Modifier,
    launcherForImageCapture: ManagedActivityResultLauncher<String, Boolean>,
    context: Context = LocalContext.current
) {

}

The composable ScreenContent( ) uses:
1) Modifier to modify the content.
2) launcherForImageCapture to pass the permissionLauncher( ) to the UI.
3) Context to launch the permission.

Now, add the following code to the ScreenContenet( ) inside your wrapper.

Image(
    bitmap = placeHolderBitmap.asImageBitmap(),
    contentDescription = "Captured image",
    contentScale = ContentScale.FillWidth,
    modifier = Modifier
        .size(150.dp)
        .clickable {
            if (isPermissionGranted(context, CAMERA_PERMISSION)) {
                // Do you thing, permission granted
            } else {
                launcherForImageCapture.launch(CAMERA_PERMISSION)
            }
    }
)

If you have list of permission instead of just one permission, you can do this way.

Image(
    bitmap = placeHolderBitmap.asImageBitmap(),
    contentDescription = "Captured image",
    contentScale = ContentScale.FillWidth,
    modifier = Modifier
        .size(150.dp)
        .clickable {
            if (isPermissionGranted(context, listOf(CAMERA_PERMISSION))) {
                // List of permission are granted
            } else {
                launcherForImageCapture.launch(
                    samplePermissions()
                        .toTypedArray()
                        .toString()
                )
            }
    }
)

The ProfileOutlinedTextField( ) is nothing but a simple OutlinedTextField( ) which looks like:

@Composable
fun ProfileOutlinedTextField(
    value: String,
    onValueChange: (String) -> Unit = { }
) {
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        modifier = Modifier.padding(vertical = 10.dp),
        textStyle = TextStyle(color = Color.Gray)
    )
}

Now, call the ScreenContent( ) in the ProfileScreen( )

@Composable
fun
ProfileScreen() {
    val launcherForImageCapture = permissionLauncher()
    ScreenContent(
        modifier = Modifier
            .fillMaxSize()
            .systemBarsPadding(),
        launcherForImageCapture = launcherForImageCapture
    )
}

6. Permission Logic

Now let us take a step further towards understanding the permissions logic implemented in the Image.

Image(
    bitmap = placeHolderBitmap.asImageBitmap(),
    contentDescription = "Captured image",
    contentScale = ContentScale.FillWidth,
    modifier = Modifier
        .size(150.dp)
        .clickable {
            if (isPermissionGranted(context, CAMERA_PERMISSION)) {
            } else {
                launcherForImageCapture.launch(
                    CAMERA_PERMISSION
                )
            }
            if (isPermissionGranted(context,
                    listOf(CAMERA_PERMISSION))) {
            } else {
                launcherForImageCapture.launch(
                    samplePermissions()
                        .toTypedArray()
                        .toString()
                )
            }
    }
)

Here, the following code added to the clickable property of the image element handles the click as follows:
1) The app checks for permission to access the camera.
2) If the permission is granted and validated by the isPermissionGranted( ) function, then the camera is launched.
3) If the permission is not granted, then the app prompts a permission dialog box, that asks the user to grant the permission to carry out the particular function which is undergoing.

Here is the working preview of the same.

Working preview.

7. Use case – If no image is clicked

If the user allows the camera to launch and capture the image, he/she is redirected to the camera. Do you wonder, if the user exits the camera without clicking any picture, what would happen?

The app would crash right?

No! Not with the code we have discussed above.

var resultBitmap: Bitmap? by rememberSaveable { mutableStateOf(placeHolderBitmap) }

    val launcherForImageCapture = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicturePreview()
    ) {
        resultBitmap = if (it.toString().isEmpty()) {
            placeHolderBitmap
        } else {
            it
        }
    }

Here our variable resultBitmap which is used to store the captured image, is a nullable! Which implies that it could hold a null value.

Further more, in the launcherForImageCapture block, the code is branched conditionally in a way that even if the captured image is a null Bitmap, then the resultBitmap stores the placeHolderBitmap.

val placeHolderBitmap: Bitmap = BitmapFactory.decodeResource(
    Resources.getSystem(),
    android.R.drawable.ic_menu_camera
)

Now, without these changes to your code, the app would end up crashing if you exit the camera without clicking a picture.

Project code and resources

Link to the code repository.

  1. Android Jetpack Compose – Android Documentation
  2. Android permissions documentation
  3. Manifest permissions documentation
  4. Accompanist library documentation
  5. Use Accompanist to implement permissions

I would also like to extend my gratitude to Raj for contributing clean, simple, and user-friendly code to this project.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

This site uses Akismet to reduce spam. Learn how your comment data is processed.