简体   繁体   中英

How to handle runtime permissions in jetpack compose properly by accompanist?

I'm using the accompanist library for handling permissions in jetpack compose. The sample code in the docs doesn't have a scenario to handle permissions such as checking permission on button clicks.

So My scenario is I wanted to check runtime permission on the button click and if the permission is granted do the required work or show the snackbar if not granted. But I can't figure out how can i check if permission was denied permanently or not.

I want a similar behavior like this library has https://github.com/Karumi/Dexter

    val getImageLauncher = rememberLauncherForActivityResult(
        contract = GetContent()
    ) { uri ->

        uri?.let {
            viewModel.imagePicked.value = it.toString()
        }
    }

    // Remember Read Storage Permission State
    val readStoragePermissionState = rememberPermissionState(
        permission = READ_EXTERNAL_STORAGE
    ) { result ->

        if (result) {
            getImageLauncher.launch("image/*")
        } else {

            // How can i check here if permission permanently denied?
            
            coroutineScope.launch {

                scaffoldState.snackbarHostState.showSnackbar(
                    context.getString(R.string.read_storage_denied)
                )
                
            }
        }
    }

Here's the code of the button on which when I click I want to check the permission

    SecondaryOutlineButton(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp),
        buttonText = stringResource(
            id = R.string.upload_image
        ),
        buttonCornerRadius = 8.dp,
    ) {
        readStoragePermissionState.launchPermissionRequest()
    }

For those looking for a similar scenario. To handle permissions in jetpack compose properly I followed the below steps:

  1. When the button is clicked first check if the permission is already granted. If it's already granted then simply do the work you needed to do.

  2. If it's not granted we will check the case for shouldShouldRational is false. If it's false we have two scenarios to check because the shouldShowRationale is false in two cases. First when the permission is permanently denied. Second when permission is not even asked at once. For managing, if permission is permanently denied or not I have used shared preferences. I have written extension functions for that which tell us if the permission is asked for once.

  3. For the above first case, I'll show the snack bar telling the user that you permanently denied the permission open settings to allow the permission. For the above second case, I will launch the request for showing the system permission dialog and update the shared preference via the extension function.

  4. And for the case in which shouldShowRationale is true. I'll show a snack bar to the user explaining why permission is required. Along with the action, to again request the system permission dialog.

  5. Finally whenever permission is granted I can do the work needed in the rememberPermissionState callback.


val context = LocalContext.current

val scaffoldState = rememberScaffoldState()

val coroutineScope = rememberCoroutineScope()

val getImageLauncher = rememberLauncherForActivityResult(
    contract = GetContent()
) { uri ->

    uri?.let {
        viewModel.imagePicked.value = it.toString()
    }
}

// Remember Read Storage Permission State
val readStoragePermissionState = rememberPermissionState(
    permission = READ_EXTERNAL_STORAGE
) { granted ->

    if (granted) {
        getImageLauncher.launch("image/*")
    }
}

Button Composable


SecondaryOutlineButton(
    modifier = Modifier
        .fillMaxWidth()
        .height(48.dp),
    buttonText = stringResource(
        id = R.string.upload_image
    ),
    buttonCornerRadius = 8.dp,
) {
    // This is onClick Callback of My Custom Composable Button
    with(readStoragePermissionState) {

        when {

            // If Permission is Already Granted to the Application
            status.isGranted -> {
                getImageLauncher.launch("image/*")
            }

            // If Permission is Asked First or Denied Permanently
            !status.shouldShowRationale -> {

                context.isPermissionAskedForFirstTime(
                    permission = permission
                ).also { result ->

                    if (result) {

                        launchPermissionRequest()

                        context.permissionAskedForFirsTime(
                            permission = permission
                        )

                    } else {

                        coroutineScope.launch {

                            with(scaffoldState.snackbarHostState) {

                                val snackbarResult =
                                    showSnackbar(
                                        message = context.getString(
                                            R.string.read_storage_denied
                                        ),
                                        actionLabel = context.getString(
                                            R.string.settings
                                        )
                                    )

                                when (snackbarResult) {
                                    // Open this Application General Settings.
                                    SnackbarResult.ActionPerformed -> {
                                        context.openApplicationSettings()
                                    }

                                    SnackbarResult.Dismissed -> Unit
                                }
                            }
                        }
                    }
                }
            }

            // If You should Tell User Why this Permission Required
            status.shouldShowRationale -> {

                coroutineScope.launch {

                    with(scaffoldState.snackbarHostState) {

                        val snackbarResult = showSnackbar(
                            message = context.getString(
                                R.string.read_storage_rational
                            ),
                            actionLabel = context.getString(
                                R.string.allow
                            )
                        )

                        when (snackbarResult) {
                            // Request for System Permission Dialog Again.
                            SnackbarResult.ActionPerformed -> {
                                launchPermissionRequest()
                            }

                            SnackbarResult.Dismissed -> Unit
                        }
                    }
                }
            }

            else -> Unit
        }
    }
}

Extension Functions

fun Context.isPermissionAskedForFirstTime(
    permission: String
): Boolean {

    return getSharedPreferences(
        packageName, MODE_PRIVATE
    ).getBoolean(permission, true)
}

fun Context.permissionAskedForFirsTime(
    permission: String
) {
    getSharedPreferences(
        packageName, MODE_PRIVATE
    ).edit().putBoolean(permission, false).apply()
}

fun Context.openApplicationSettings() {
    startActivity(Intent().apply {
        action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
        data = Uri.parse("package:${packageName}")
    })
}

I'm using implementation "com.google.accompanist:accompanist-permissions:0.25.0"

I used Philipp Lackner's tutorial for this. He creates an extension method in case the permission is permanently denied.

So in your button Code you would have a method doing this:

Manifest.permission.CAMERA -> {
                    when {
                        perm.status.isGranted -> {                                                                 
                          PermissionText(text = "Camera permission accepted.")
                          }

                        perm.status.shouldShowRationale -> {
                            PermissionText(text = "Camera permission is needed to take pictures.")
                        }

                        perm.isPermanentlyDenied() -> {
                            PermissionText(text = "Camera permission was permanently denied. You can enable it in the app settings.")
                        }
                    }
                }

And the extension would be:

@ExperimentalPermissionsApi
fun PermissionState.isPermanentlyDenied(): Boolean {
    return !status.shouldShowRationale && !status.isGranted
}

Here is the code that does exactly what you are asking:

Click a button (FAB), if the permission is already granted, start working. If the permission is not granted, check if we need to display more info to the user (shouldShowRationale) before requesting and display a SnackBar if needed. Otherwise just ask for the permission (and start work if then granted).

Keep in mind that it is no longer possible to check if a permission is permanently denied. shouldShowRationale() works differently in different versions of Android. What you can do instead (see code), is to display your SnackBar if shouldShowRationale() returns true.

@Composable
fun OptionalPermissionScreen() {
    val context = LocalContext.current.applicationContext

    val state = rememberPermissionState(Manifest.permission.CAMERA)
    val scaffoldState = rememberScaffoldState()
    val launcher = rememberLauncherForActivityResult(RequestPermission()) { wasGranted ->
        if (wasGranted) {
            // TODO do work (ie forward to viewmodel)
            Toast.makeText(context, "📸 Photo in 3..2..1", Toast.LENGTH_SHORT).show()
        }
    }
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        scaffoldState = scaffoldState,
        floatingActionButton = {
            val scope = rememberCoroutineScope()
            val snackbarHostState = scaffoldState.snackbarHostState

            FloatingActionButton(onClick = {
                when (state.status) {
                    PermissionStatus.Granted -> {
                        // TODO do work (ie forward to viewmodel)
                        Toast.makeText(context, "📸 Photo in 3..2..1", Toast.LENGTH_SHORT).show()
                    }
                    else -> {
                        if (state.status.shouldShowRationale) {
                            scope.launch {
                                val result =
                                    snackbarHostState.showSnackbar(
                                        message = "Permission required",
                                        actionLabel = "Go to settings"
                                    )
                                if (result == SnackbarResult.ActionPerformed) {
                                    val intent = Intent(
                                        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                                        Uri.fromParts("package", context.packageName, null)
                                    )
                                    startActivity(intent)
                                }
                            }
                        } else {
                            launcher.launch(Manifest.permission.CAMERA)
                        }
                    }
                }
            }) {
                Icon(Icons.Rounded.Camera, contentDescription = null)
            }
        }) {
        // the rest of your screen
    }
}

Video of how this works by clicking here .

this is part of a blog post I wrote on permissions in Jetpack Compose .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM