The introduction of Jetpack Compose changes the way we build UI in Android. It simplifies and accelerates UI development in Android. One component of Android UI development that also gets affected by this change is how we handle permissions in Android. The Compose Accompanist Permission library provides easy-to-use Compose API for handling permission in a Jetpack Compose Architecture. However, it doesn’t take away the boilerplates of writing repeated codes to handle permissions.
In this article, I will show you how to create a reusable permissions handler for Jetpack Compose UI. We will be using the Accompanist Permission Library to create this permissions handler class.
implementation("com.google.accompanist:accompanist-permissions:0.23.1") |
Let’s start by creating a “PermissionsHandler” class that will handle all permission-related Events by invoking a State update which in turn triggers our custom permission composable to perform the requested Permission Action. Here is how the PermissionsHandler looks like
class PermissionsHandler { | |
private val _state = MutableStateFlow(State()) | |
val state: StateFlow<State> = _state | |
fun onEvent(event: Event) { | |
when (event) { | |
Event.PermissionDenied -> onPermissionDenied() | |
Event.PermissionDismissTapped -> onPermissionDismissTapped() | |
Event.PermissionNeverAskAgain -> onPermissionNeverShowAgain() | |
Event.PermissionRationaleOkTapped -> onPermissionRationaleOkTapped() | |
Event.PermissionRequired -> onPermissionRequired() | |
Event.PermissionSettingsTapped -> onPermissionSettingsTapped() | |
Event.PermissionsGranted -> onPermissionGranted() | |
is Event.PermissionsStateUpdated -> onPermissionsStateUpdated(event.permissionsState) | |
} | |
} | |
data class State( | |
val multiplePermissionsState: MultiplePermissionsState? = null, | |
val permissionAction: Action = Action.NO_ACTION | |
) | |
sealed class Event { | |
object PermissionDenied : Event() | |
object PermissionsGranted : Event() | |
object PermissionSettingsTapped : Event() | |
object PermissionNeverAskAgain : Event() | |
object PermissionDismissTapped : Event() | |
object PermissionRationaleOkTapped : Event() | |
object PermissionRequired : Event() | |
data class PermissionsStateUpdated(val permissionsState: MultiplePermissionsState) : | |
Event() | |
} | |
enum class Action { | |
REQUEST_PERMISSION, SHOW_RATIONALE, SHOW_NEVER_ASK_AGAIN, NO_ACTION | |
} | |
} |
We have just 2 variables in the Permission Handler “State”, the “multiplePermissionsState” provides information about the state of the permissions required. This property would tell us if a required permission is denied or granted, or if a permission rationale screen needs to be displayed.
We get the “multiplePermissionsState” from the Accompanist Permission API “rememberMultiplePermissionsState”, then passed to the “PermissionHandler.State” via the “PermissionStateUpdated” event. Very soon we will see how the event “PermissionStateUpdated” and other events are being dispatched. For now, let’s implement the methods to handle those Events.
class PermissionsHandler { | |
private fun onPermissionsStateUpdated(permissionState: MultiplePermissionsState) { | |
_state.update { it.copy(multiplePermissionsState = permissionState) } | |
} | |
private fun onPermissionGranted() { | |
_state.update { it.copy(permissionAction = Action.NO_ACTION) } | |
} | |
private fun onPermissionDenied() { | |
_state.update { it.copy(permissionAction = Action.NO_ACTION) } | |
} | |
private fun onPermissionNeverShowAgain() { | |
_state.update { | |
it.copy(permissionAction = Action.SHOW_NEVER_ASK_AGAIN) | |
} | |
} | |
private fun onPermissionRequired() { | |
_state.value.multiplePermissionsState?.let { | |
val permissionAction = | |
if (!it.allPermissionsGranted && !it.shouldShowRationale && !it.permissionRequested) { | |
Action.REQUEST_PERMISSION | |
} else if (!it.allPermissionsGranted && it.shouldShowRationale) { | |
Action.SHOW_RATIONALE | |
} else { | |
Action.SHOW_NEVER_ASK_AGAIN | |
} | |
_state.update { it.copy(permissionAction = permissionAction) } | |
} | |
} | |
private fun onPermissionRationaleOkTapped() { | |
_state.update { it.copy(permissionAction = Action.REQUEST_PERMISSION) } | |
} | |
private fun onPermissionDismissTapped() { | |
_state.update { it.copy(permissionAction = Action.NO_ACTION) } | |
} | |
private fun onPermissionSettingsTapped() { | |
_state.update { it.copy(permissionAction = Action.NO_ACTION) } | |
} | |
} |
For the “PermissionRequired” event, we check the “multiplePermissionsState” to decide the appropriate permission action to use.
Now let’s create a composable that will handle the “permissionAction”
@Composable | |
fun HandlePermissionAction( | |
action: PermissionsHandler.Action, | |
permissionStates: MultiplePermissionsState?, | |
@StringRes rationaleText: Int, | |
@StringRes neverAskAgainText: Int, | |
onOkTapped: () -> Unit, | |
onSettingsTapped: () -> Unit, | |
) { | |
val context = LocalContext.current | |
when (action) { | |
PermissionsHandler.Action.REQUEST_PERMISSION -> { | |
LaunchedEffect(true) { | |
permissionStates?.launchMultiplePermissionRequest() | |
} | |
} | |
PermissionsHandler.Action.SHOW_RATIONALE -> { | |
PermissionRationaleDialog( | |
message = stringResource(rationaleText), | |
onOkTapped = onOkTapped | |
) | |
} | |
PermissionsHandler.Action.SHOW_NEVER_ASK_AGAIN -> { | |
ShowGotoSettingsDialog( | |
title = stringResource(R.string.allow_permission), | |
message = stringResource(neverAskAgainText), | |
onSettingsTapped = { | |
onSettingsTapped() | |
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { | |
data = Uri.parse("package:" + context.packageName) | |
context.startActivity(this) | |
} | |
}, | |
) | |
} | |
PermissionsHandler.Action.NO_ACTION -> Unit | |
} | |
} |
Job Offers
“PermissionRationaleDialog” and “ShowGotoSettingsDialog” are custom composables I created. Those can be replaced with your preferred composables. When the permission action is “REQUEST_PERMISSION” we invoke the “launchMultiplePermissionRequest” method from the accompanist library.
The final step for us is to create a “HandlePermissionsRequest” composable that can be slotted into any Compose UI. The composable takes in a PermissionHandler as a parameter along with a list of required permissions. The 2 arguments can be passed from the parent composable.
@Composable | |
fun HandlePermissionsRequest(permissions: List<String>, permissionsHandler: PermissionsHandler) { | |
val state by permissionHandler.state.collectAsState() | |
val permissionsState = rememberMultiplePermissionsState(permissions) | |
LaunchedEffect(permissionsState) { | |
permissionHandler.onEvent(PermissionHandler.Event.PermissionsStateUpdated(permissionsState)) | |
when { | |
permissionsState.allPermissionsGranted -> { | |
permissionsHandler.onEvent(PermissionsHandler.Event.PermissionsGranted) | |
} | |
permissionsState.permissionRequested && !permissionsState.shouldShowRationale -> { | |
permissionsHandler.onEvent(PermissionsHandler.Event.PermissionNeverAskAgain) | |
} | |
else -> { | |
permissionsHandler.onEvent(PermissionsHandler.Event.PermissionDenied) | |
} | |
} | |
} | |
HandlePermissionAction( | |
action = state.permissionAction, | |
permissionStates = state.multiplePermissionsState, | |
rationaleText = R.string.permission_rationale, | |
neverAskAgainText = R.string.permission_rationale, | |
onOkTapped = { permissionsHandler.onEvent(PermissionsHandler.Event.PermissionRationaleOkTapped) }, | |
onSettingsTapped = { permissionsHandler.onEvent(PermissionsHandler.Event.PermissionSettingsTapped) }, | |
) | |
} |
Reviewing the code above, we see how “Events” are dispatched to the “PermissionHandler”, including the “PermissionsStateUpdated” event. A change to “permissionsState” will cause the LaunchedEffect compose API to be executed.
At this point, we have created a reusable permission-handling class for any Compose UI. To use this permissions handler, we can either create the PermissionHandler directly on the Compose UI, or have it as a ViewModel argument. In the complete sample code published on Github, I referenced the “PermissionHandler” via the ViewModel. The snippet below shows how to reference it directly on the Compose UI
@Composable | |
internal fun SampleScreen() { | |
val permissions = remember { listOf(Manifest.permission.CAMERA) } | |
val permissionsHandler = remember(permissions) { PermissionsHandler() } | |
val permissionsStates by permissionsHandler.state.collectAsState() | |
HandlePermissionsRequest(permissions = permissions, permissionsHandler = permissionsHandler) | |
Box { | |
if (permissionsStates.multiplePermissionsState?.allPermissionsGranted == true) { | |
Text(text = "Permission Granted") | |
} else { | |
Button (onClick = {permissionsHandler.onEvent(PermissionsHandler.Event.PermissionRequired)}) { | |
Text(text = "Request Permission") | |
} | |
} | |
} | |
} |
Premise is constantly looking to hire top-tier engineering talent to help us improve our front-end, mobile offerings, data and cloud infrastructure. Come find out why we’re consistently named among the top places to work in Silicon Valley by visiting our Careers page.
This article was originally published on proandroiddev.com on August 01, 2022