How request permissions with Jetpack Compose?

Viewed 17888

How should be implemented requesting permission from Jetpack Compose View? I'm trying implement application accessing Camera with Jetpack Compose. I tried example from How to get Current state or context in Jetpack Compose Unfortunately example is no longer working with dev06.

        fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
    }
}
10 Answers

as compose_version = '1.0.0-beta04' and

implementation 'androidx.activity:activity-compose:1.3.0-alpha06'

you can do request permission as simple as this:

@Composable
fun ExampleScreen() {
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // Permission Accepted: Do something
            Log.d("ExampleScreen","PERMISSION GRANTED")

        } else {
            // Permission Denied: Do something
            Log.d("ExampleScreen","PERMISSION DENIED")
        }
    }
    val context = LocalContext.current

    Button(
        onClick = {
            // Check permission
            when (PackageManager.PERMISSION_GRANTED) {
                ContextCompat.checkSelfPermission(
                    context,
                    Manifest.permission.READ_EXTERNAL_STORAGE
                ) -> {
                    // Some works that require permission
                    Log.d("ExampleScreen","Code requires permission")
                }
                else -> {
                    // Asking for permission
                    launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
                }
            }
        }
    ) {
        Text(text = "Check and Request Permission")
    }
}

Google has a library called "Accompanist". It has many help libraries and one of them is the Permission Library.

Check: Library: https://github.com/google/accompanist/

Documentation: https://google.github.io/accompanist/permissions/

Example:

Setup in build.gradle file:

repositories {
    mavenCentral()
}

dependencies {

implementation "com.google.accompanist:accompanist-permissions:<latest_version>"

}

Implementation in Code

@Composable
private fun FeatureThatRequiresCameraPermission() {

    // Camera permission state
    val cameraPermissionState = rememberPermissionState(
        android.Manifest.permission.CAMERA
    )

    when (cameraPermissionState.status) {
        // If the camera permission is granted, then show screen with the feature enabled
        PermissionStatus.Granted -> {
            Text("Camera permission Granted")
        }
        is PermissionStatus.Denied -> {
            Column {
                val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
                    // If the user has denied the permission but the rationale can be shown,
                    // then gently explain why the app requires this permission
                    "The camera is important for this app. Please grant the permission."
                } else {
                    // If it's the first time the user lands on this feature, or the user
                    // doesn't want to be asked again for this permission, explain that the
                    // permission is required
                    "Camera permission required for this feature to be available. " +
                        "Please grant the permission"
                }
                Text(textToShow)
                Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
                    Text("Request permission")
                }
            }
        }
    }
}
/**
 * Composable helper for permission checking
 *
 * onDenied contains lambda for request permission
 *
 * @param permission permission for request
 * @param onGranted composable for [PackageManager.PERMISSION_GRANTED]
 * @param onDenied composable for [PackageManager.PERMISSION_DENIED]
 */
@Composable
fun ComposablePermission(
    permission: String,
    onDenied: @Composable (requester: () -> Unit) -> Unit,
    onGranted: @Composable () -> Unit
) {
    val ctx = LocalContext.current

    // check initial state of permission, it may be already granted
    var grantState by remember {
        mutableStateOf(
            ContextCompat.checkSelfPermission(
                ctx,
                permission
            ) == PackageManager.PERMISSION_GRANTED
        )
    }
    if (grantState) {
        onGranted()
    } else {
        val launcher: ManagedActivityResultLauncher<String, Boolean> =
            rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
                grantState = it
            }
        onDenied { launcher.launch(permission) }
    }
}

A little late but this might help as I had the problem today:

With ContextAmbient.current it is not guaranteed that you have an activity or fragment thus I created my own ambient for handling permissions.

val AmbientPermissionHandler: ProvidableAmbient<PermissionHandler> =
    ambientOf { throw IllegalStateException("permission handler is not initialized") }

// Activity:
private val permissionHandler = PermissionHandler(this)
// onCreate:
setContent {
    Providers(
        AmbientPermissionHandler provides permissionHandler
    ) {/* Composable Contnent */}

Usage:

@Composable
fun PermissionHandler(
    permissions: Array<out String>,
    requestCode: Int,
    granted: @Composable() () -> Unit,
    denied: @Composable() () -> Unit,
    deniedPermanently: (@Composable() () -> Unit)? = null,
    rational: (@Composable() () -> Unit)? = null,
    awaitResult: (@Composable() () -> Unit)? = null,
) {
    val permissionHandler = AmbientPermissionHandler.current
    val (permissionResult, setPermissionResult) = remember(permissions) { mutableStateOf<PermissionResult?>(null) }
    LaunchedEffect(Unit) {
        setPermissionResult(permissionHandler.requestPermissionsSuspend(requestCode, permissions))
    }
    when (permissionResult) {
        is PermissionResult.PermissionGranted -> granted()
        is PermissionResult.PermissionDenied -> denied()
        is PermissionResult.PermissionDeniedPermanently -> deniedPermanently?.invoke()
        is PermissionResult.ShowRational -> rational?.invoke()
        null -> awaitResult?.invoke()
    }
}

Implementation of PermissionHandler with dependency https://github.com/sagar-viradiya/eazypermissions

class PermissionHandler(
    private val actualHandler: AppCompatActivity,
) {
    suspend fun requestPermissionsSuspend(requestCode: Int, permissions: Array<out String>): PermissionResult {
        return PermissionManager.requestPermissions(actualHandler, requestCode, *permissions)
    }

    fun requestPermissionsWithCallback(requestCode: Int, permissions: Array<out String>, onResult: (PermissionResult) -> Unit) {
        actualHandler.lifecycleScope.launch {
            onResult.invoke(PermissionManager.requestPermissions(actualHandler, requestCode, *permissions))
        }
    }
}

If you prefer a callback the second function works also.

Don't forget to add

<uses-permission android:name="android.permission.CAMERA"/>

(when requesting camera permission) to your AndroidManifest.xml, otherwise it might end up with the permission denied state, when using the solutions others provided.

private const val PERMISSIONS_REQUEST_CODE = 10
private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)

@Composable
fun PermissionButton() {

    val context = ContextAmbient.current

    Button(onClick = {
        if (!hasPermissions(context)) {
            requestPermissions(
                context as Activity,
                PERMISSIONS_REQUIRED,
                PERMISSIONS_REQUEST_CODE
            )
        }
    }
    ) {}
}

fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
    ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
//define permission in composable fun
val getPermission = rememberLauncherForActivityResult( 
    ActivityResultContracts.RequestPermission()
) { isGranted ->
    if (isGranted) {
       //permission accepted do somthing
    } else {
       //permission not accepted show message
    }
}
//i used SideEffect to launch permission request when screen recomposed
//you can call it inside a button click without SideEffect
SideEffect {
    getPermission.launch(Manifest.permission.READ_CONTACTS)
}

and if you wanted to request multiple permission use this:

ActivityResultContracts.RequestMultiplePermissions()

Permission Sample UI

The rememberPermissionState(permission: String) API allows you to request a certain permission to the user and check for the status of the permission.

**Step1:**

A library which provides Android runtime permissions support for Jetpack Compose.

implementation 'com.google.accompanist:accompanist-permissions:0.24.13-rc'
..

**Step2:**

In our AndroidManifeastxml we need to declare permission (in this example we are going to request location permission)

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
..

**Step3:**

In our MainActivity.kt we are calling this permission request function

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalPermissionsApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Material3ComposeTheme {
                RequestPermission(permission = Manifest.permission.ACCESS_FINE_LOCATION)
            }
        }
    }
}
..

**Step4:**

In this SinglePermission.kt, we are going to request permission from user, if user already deny means we will show simple alert dialog info message otherwise will show custom full screen dialog.

package compose.material.theme

import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import com.google.accompanist.permissions.*

@ExperimentalPermissionsApi
@Composable
fun RequestPermission(
    permission: String,
    rationaleMessage: String = "To use this app's functionalities, you need to give us the permission.",
) {
    val permissionState = rememberPermissionState(permission)

    HandleRequest(
        permissionState = permissionState,
        deniedContent = { shouldShowRationale ->
            PermissionDeniedContent(
                rationaleMessage = rationaleMessage,
                shouldShowRationale = shouldShowRationale
            ) { permissionState.launchPermissionRequest() }
        },
        content = {
         /*   Content(
                text = "PERMISSION GRANTED!",
                showButton = false
            ) {}*/
        }
    )
}

@ExperimentalPermissionsApi
@Composable
fun HandleRequest(
    permissionState: PermissionState,
    deniedContent: @Composable (Boolean) -> Unit,
    content: @Composable () -> Unit
) {
    when (permissionState.status) {
        is PermissionStatus.Granted -> {
            content()
        }
        is PermissionStatus.Denied -> {
            deniedContent(permissionState.status.shouldShowRationale)
        }
    }
}

@Composable
fun Content(showButton: Boolean = true, onClick: () -> Unit) {
    if (showButton) {
        val enableLocation = remember { mutableStateOf(true) }
        if (enableLocation.value) {
            CustomDialogLocation(
                title = "Turn On Location Service",
                desc = "Explore the world without getting lost and keep the track of your location.\n\nGive this app a permission to proceed. If it doesn't work, then you'll have to do it manually from the settings.",
                enableLocation,
                onClick
            )
        }
    }
}

@ExperimentalPermissionsApi
@Composable
fun PermissionDeniedContent(
    rationaleMessage: String,
    shouldShowRationale: Boolean,
    onRequestPermission: () -> Unit
) {

    if (shouldShowRationale) {

        AlertDialog(
            onDismissRequest = {},
            title = {
                Text(
                    text = "Permission Request",
                    style = TextStyle(
                        fontSize = MaterialTheme.typography.headlineLarge.fontSize,
                        fontWeight = FontWeight.Bold
                    )
                )
            },
            text = {
                Text(rationaleMessage)
            },
            confirmButton = {
                Button(onClick = onRequestPermission) {
                    Text("Give Permission")
                }
            }
        )
        
    }
    else {
        Content(onClick = onRequestPermission)
    }

}
.. 

**Step 5:**

In this CustomDialogLocation.kt, we make custom dialog in android jetpack compose.

package compose.material.theme

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog

/*

This example demonstrates how to make custom dialog in android jetpack compose in android.

*  Button        : https://www.boltuix.com/2021/12/button_25.html
*  Clip Modifier : https://www.boltuix.com/2021/12/clip-modifier_24.html
*  Alert Dialog  : https://www.boltuix.com/2021/12/alert-dialog_25.html
*  Column        : https://www.boltuix.com/2021/12/column-layout_25.html
*  Box           : https://www.boltuix.com/2021/12/box-layout_25.html
*  Type.kt       : https://www.boltuix.com/2021/12/typography_27.html
*  Color.kt      : https://www.boltuix.com/2022/05/google-material-design-color.html
*  Dialog        : https://www.boltuix.com/2022/07/compose-custom-animating-dialog.html
* */

@Composable
fun CustomDialogLocation(
    title: String? = "Message",
    desc: String? = "Your Message",
    enableLocation: MutableState<Boolean>,
    onClick: () -> Unit
) {
    Dialog(
        onDismissRequest = { enableLocation.value = false}
    ) {
        Box(
            modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)
                // .width(300.dp)
                // .height(164.dp)
                .background(
                    color = MaterialTheme.colorScheme.onPrimary,
                    shape = RoundedCornerShape(25.dp,5.dp,25.dp,5.dp)
                )
                .verticalScroll(rememberScrollState())

        ) {
            Column(
                modifier = Modifier.padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {


                //.........................Image: preview
                Image(
                    painter = painterResource(id = R.drawable.permission_location),
                    contentDescription = null,
                    contentScale = ContentScale.Fit,
                    /*  colorFilter  = ColorFilter.tint(
                          color = MaterialTheme.colorScheme.primary
                      ),*/
                    modifier = Modifier
                        .padding(top = 5.dp)
                        .height(320.dp)
                        .fillMaxWidth(),

                    )
                //.........................Spacer
                //.........................Text: title
                Text(
                    text = title!!,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                      //  .padding(top = 5.dp)
                        .fillMaxWidth(),
                    letterSpacing = 2.sp,
                    fontWeight = FontWeight.Bold,
                    style = MaterialTheme.typography.titleLarge,
                    color = MaterialTheme.colorScheme.primary,
                )
                Spacer(modifier = Modifier.height(8.dp))
                //.........................Text : description
                Text(
                    text = desc!!,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .padding(top = 10.dp, start = 25.dp, end = 25.dp)
                        .fillMaxWidth(),
                    letterSpacing = 1.sp,
                    style = MaterialTheme.typography.bodyLarge,
                    color = MaterialTheme.colorScheme.primary,
                )
                //.........................Spacer
                Spacer(modifier = Modifier.height(24.dp))

                //.........................Button : OK button
                val cornerRadius = 16.dp
                val gradientColors = listOf(Color(0xFFff669f), Color(0xFFff8961))
                val roundedCornerShape = RoundedCornerShape(topStart = 30.dp,bottomEnd = 30.dp)

                Button(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 32.dp, end = 32.dp),
                    onClick=onClick,
                    contentPadding = PaddingValues(),
                    colors = ButtonDefaults.buttonColors(
                        containerColor = Color.Transparent
                    ),
                    shape = RoundedCornerShape(cornerRadius)
                ) {

                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(
                                brush = Brush.horizontalGradient(colors = gradientColors),
                                shape = roundedCornerShape
                            )
                            .padding(horizontal = 16.dp, vertical = 8.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        Text(
                            text ="Enable",
                            fontSize = 20.sp,
                            color = Color.White
                        )
                    }
                }


                //.........................Spacer
                Spacer(modifier = Modifier.height(12.dp))


                TextButton(onClick = {
                    enableLocation.value = false
                }) { Text("Cancel", style = MaterialTheme.typography.labelLarge) }


                Spacer(modifier = Modifier.height(24.dp))

            }
        }
    }
}

Get source code & video: https://www.boltuix.com/2022/07/requesting-location-permission-in.html

You can request multiples permissions.

class MainActivity : ComponentActivity() {

    private val neededPermissions = arrayOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
    )

    @OptIn(ExperimentalMaterialApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MfmTheme {
                val launcher = rememberLauncherForActivityResult(
                    contract = ActivityResultContracts.RequestMultiplePermissions()
                ) { maps ->
                    val granted = maps.values.reduce { acc, next -> (acc && next) }
                    if (granted) {
                        // all permission granted
                    } else {
                        // Permission Denied: Do something
                    }
                    // You can check one by one
                    maps.forEach { entry ->
                        Log.i("Permission = ${entry.key}", "Enabled ${entry.value}")
                    }
                }
                val context = LocalContext.current
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background,
                    onClick = {
                        when {
                            hasPermissions(context, *neededPermissions) -> {
                                // All permissions granted
                               
                            }
                            else -> {
                                // Request permissions
                                launcher.launch(neededPermissions)
                            }
                        }
                    }
                ) {
                    Greeting("Android")
                }
            }
        }
    }

    private fun hasPermissions(context: Context, vararg permissions: String): Boolean =
        permissions.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MfmTheme {
        Greeting("Android")
    }
}
Related