For those looking for a similar scenario. To handle permissions in jetpack compose properly I followed the below steps:
- 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.
- If it's not granted we will request the permission. And in the callback of
rememberPermissionState(){granted -> }
we will check if the permission is granted or not.
- If permission is granted then simply do the work you needed to do. Otherwise, we have 2 scenarios to check now. Check if the
shouldShowRequestPermissionRationale
returns true or false.
- As we have requested the permission already so we already neglected the scenario when
shouldShowRequestPermissionRationale
returns false for the first time before requesting the permission.
- So if the
shouldShowRequestPermissionRationale
returns false that means the permission is permanently denied we can show a message to the user to go to settings to grant permission otherwise it might be denied for once only then we can show some message to the user telling why we need access to that permission.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val snackBarState = remember { SnackbarHostState() }
val getImageLauncher = rememberLauncherForActivityResult(
contract = GetContent()
) { uri ->
//Todo
}
// Remember Read Storage Permission State
val readStoragePermissionState = rememberPermissionState(
permission = READ_EXTERNAL_STORAGE
) { granted ->
if (granted) {
getImageLauncher.launch("image/*")
} else {
context.findActivity()?.apply {
when {
shouldShowRationale(READ_EXTERNAL_STORAGE) -> {
snackbarState.showSnackBar(
message = context.getString(
R.string.read_storage_rational
),
coroutineScope = coroutineScope,
)
}
else -> {
snackbarState.showSnackBar(
action = context.getString(
R.string.settings
),
message = context.getString(
R.string.read_storage_denied
),
coroutineScope = coroutineScope,
onSnackBarAction = {
context.gotoApplicationSettings()
},
)
}
}
}
}
}
Button Composable
SecondaryOutlineButton(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
buttonText = stringResource(
id = R.string.upload_image
),
buttonCornerRadius = 8.dp,
) {
if (context.hasPickMediaPermission()) {
launcher.launch(
getImageLauncher.launch("image/*")
)
} else {
permission.launchPermissionRequest()
}
}
Extension Functions
fun Context.isPermissionGranted(name: String): Boolean {
return ContextCompat.checkSelfPermission(
this, name
) == PackageManager.PERMISSION_GRANTED
}
fun Activity.shouldShowRationale(name: String): Boolean {
return shouldShowRequestPermissionRationale(name)
}
fun Context.hasPickMediaPermission(): Boolean {
return when {
// If Android Version is Greater than Android Pie!
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> true
else -> isPermissionGranted(name = READ_EXTERNAL_STORAGE)
}
}
fun Context.gotoApplicationSettings() {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.parse("package:${packageName}")
})
}
fun Context.findActivity(): Activity? {
return when (this) {
is Activity -> this
is ContextWrapper -> {
baseContext.findActivity()
}
else -> null
}
}
fun SnackbarHostState.showSnackBar(
message: String? = null,
action: String? = null,
duration: SnackbarDuration = Short,
coroutineScope: CoroutineScope,
onSnackBarAction: () -> Unit = {},
onSnackBarDismiss: () -> Unit = {},
) {
if (!message.isNullOrEmpty()) {
coroutineScope.launch {
when (showSnackbar(
message = message,
duration = duration,
actionLabel = action,
withDismissAction = duration == Indefinite,
)) {
SnackbarResult.Dismissed -> onSnackBarDismiss.invoke()
SnackbarResult.ActionPerformed -> onSnackBarAction.invoke()
}
}
}
}
I'm using implementation "com.google.accompanist:accompanist-permissions:0.25.0"