Efficiently Building App Permission Flows with the Compose Permission Request Template
Foreword
In traditional Android development, permission requests rely on requestPermissions() and onRequestPermissionsResult() methods within the Activity. Permission request logic is often concentrated in the Activity, and the callback-based pattern results in fragmented code structures.
With the rise of Jetpack Compose, we need a permission handling method that is more aligned with the declarative UI paradigm. The officially recommended Activity Result API (via rememberLauncherForActivityResult) decouples the request from the result, allowing our Compose functions to handle permissions in a state-driven manner.
While permission handling in Jetpack Compose follows declarative principles, writing the full sequence of Permission Check → Launcher Registration → Rationale Determination → Permanent Denial Handling for every new permission remains time-consuming and repetitive.
For efficient development, the goal of this article is to templatize and encapsulate this flow. This article will provide a highly reusable, officially recommended Compose permission request general template, helping developers quickly and elegantly set up permission request flows in any application.
Article Objectives
| Objective | Key Content | Problem Solved |
|---|---|---|
| General Templatization | Encapsulate permission request, state, rationale display, and permanent denial handling into one reusable Composable. | Avoid repetitive writing of large amounts of permission check and callback logic. |
| High Reusability | Design functions with flexible parameters, requiring only the permission name and the granted callback to be used. | Applicable to all dangerous permission request scenarios in the app. |
| State-Driven Design | The internal implementation follows Compose State principles, ensuring responsive UI and accurate state. |
Guarantees that the permission UI and app state are always synchronized, preventing bugs. |
| Official Recommended Core | The template is based on Activity Result API and shouldShowRationale best practices. |
Ensures code robustness and future-proofing. |
Basic Setup: Manifest Declaration and Dependency Inclusion
Regardless of the approach, all required permissions (Dangerous Permissions) must first be declared in the AndroidManifest.xml file.
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>
Introduce Compose Activity Dependency
Ensure your project includes the Activity Result API dependency for Compose:
// build.gradle (app level)
dependencies {
// Must include; provides core APIs like rememberLauncherForActivityResult
implementation("androidx.activity:activity-compose:1.9.0") // Please use the latest version
// If you need to handle multiple permissions, you can use this Contract
// implementation("androidx.activity:activity-ktx:1.9.0")
}
Core Practice: Building the Permission Request Template with rememberLauncherForActivityResult
The core of requesting permissions in Compose is using rememberLauncherForActivityResult. This is a Composable function responsible for registering an ActivityResultLauncher and automatically unregistering it when the component is removed. We build our permission request template based on this API.
Activity Context Retrieval Utility
Since the critical shouldShowRationale API in permission requests relies on an Activity, we first define a useful Context extension function to retrieve it.
// Place this in your utility class for global access
fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
// Permission requests must be made within an Activity Context
throw IllegalStateException("Permissions must be requested within an Activity Context.")
}
Template Code Implementation
We encapsulate all permission handling logic into a Composable function named PermissionRequestTemplate. It requires three core parameters:
-
permission: The string name of the permission to request. -
content: The core feature UI to display after the permission is granted. -
rationaleContent: The UI to display when explaining the permission rationale.
@Composable
fun PermissionRequestTemplate(
permission: String,
text:String= stringResource(id = R.string.request_permission),
// content: @Composable () -> Unit, // Content to show after permission is granted
rationaleContent: @Composable (
// Action to re-request after rationale explanation
onRequestPermission: () -> Unit,
// Action to navigate to settings after permanent denial
onOpenSettings: () -> Unit
) -> Unit,
// Action to navigate to settings after permanent denial
onOpenSettings: () -> Unit
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
// ① Core State: Tracks whether the permission has been granted
var isPermissionGranted by remember {
mutableStateOf(ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED)
}
// Core State: Tracks whether the "Rationale Explanation" UI needs to be shown
var showRationaleDialog by remember { mutableStateOf(false) }
var showPermissionPromptDialog by remember { mutableStateOf(true) }
// ② Register the permission request launcher
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted ->
isPermissionGranted = isGranted // Update state
// If denied, check if Rationale should be shown (state needs update even if system dialog is closed)
if (!isGranted) {
val activity = context.findActivity()
showRationaleDialog = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
}
)
// ③ Permission Status Check and UI Rendering
when {
// Status 1: Permission is granted -> Show core feature
isPermissionGranted -> {
//content()
}
// Status 2: Rationale needs to be shown -> Display Rationale Explanation UI
showRationaleDialog -> {
RationaleDisplay(
rationaleContent = rationaleContent,
onDismiss = { showRationaleDialog = false },
onRequest = {
// Re-launch the request
permissionLauncher.launch(permission)
showRationaleDialog = false // Hide dialog after request is launched
},
onOpenSettings=onOpenSettings
)
}
// Status 3: Initial or Denied state -> Show request button/prompt
else -> {
// First request, or user has permanently denied (shouldShowRationale = false)
if(showPermissionPromptDialog){
AlertDialog(
onDismissRequest = {showPermissionPromptDialog=false},
title = {
Text(text)
},
text = {
},
confirmButton = {
TextButton(onClick = {
val activity = context.findActivity()
// Re-check if Rationale needs to be shown
if (ActivityCompat.shouldShowRequestPermissionRationale(
activity,
permission
)
) {
showRationaleDialog = true
} else {
// First request, or user has permanently denied, launch request directly
permissionLauncher.launch(permission)
}
}) {
Text(stringResource(id = R.string.ok))
}
},
dismissButton = {
TextButton(onClick = {
showPermissionPromptDialog=false
}) {
Text(stringResource(id = R.string.cancel))
}
}
)
}
}
}
}
Auxiliary Composable Encapsulation (Button and Rationale)
To keep PermissionRequestTemplate concise, the request button and rationale display are encapsulated.
@Composable
fun RationaleDisplay(
rationaleContent: @Composable (onRequestPermission: () -> Unit, onOpenSettings: () -> Unit) -> Unit,
onDismiss: () -> Unit,
onRequest: () -> Unit,
onOpenSettings: () -> Unit
) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = onDismiss,
title = {
},
text = {
rationaleContent(
onRequestPermission = onRequest, // Action to re-request
onOpenSettings = onOpenSettings // Action to navigate to settings
)
},
confirmButton = {
},
dismissButton = {
TextButton(onClick = {onDismiss()}) {
Text(text = stringResource(id = R.string.cancel))
}
}
)
}
Template Usage: Concise and Efficient Call Example
Using this template makes the business Composable simpler and more aligned with the declarative style.
PermissionRequestTemplate(
permission = Manifest.permission.WRITE_EXTERNAL_STORAGE,
text= stringResource(id = R.string.request_write_permission),
rationaleContent = { onRequest, onOpenSettings ->
Column {
Text(text = stringResource(id = R.string.read_and_write_permissions), fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp))
Row {
// Guide user to request again
Button(onClick = onRequest) { Text(stringResource(id = R.string.reauthorization)) }
Spacer(modifier = Modifier.width(8.dp))
// Guide user to settings page (handles permanent denial scenario)
Button(onClick = onOpenSettings) { Text(stringResource(id = R.string.go_to_settings)) }
}
}
},
onOpenSettings = {
val packageName = context.packageName
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.data = Uri.fromParts("package", packageName, null)
startActivity(context, intent, null)
}
)
Through PermissionRequestTemplate and its auxiliary Composable, we achieve the goals for Compose permission requests: standardized flow, centralized logic, and minimized code.
Differences in Different Versions
Unfortunately, permission checking and requesting methods in Android vary across different system versions. This means developers cannot exclusively rely on the rememberLauncherForActivityResult code for all permission request logic; code implementation differs between versions.
Taking storage permission as an example (Google officially discourages apps from obtaining full storage access, recommending their file picker instead): Before Android 11, storage permission was divided into Manifest.permission.WRITE_EXTERNAL_STORAGE (write) and Manifest.permission.READ_EXTERNAL_STORAGE (read), and could be checked directly using checkSelfPermission and rememberLauncherForActivityResult.
// Check permission
if((ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED)){
//...
}
// Register permission request launcher
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission(),
onResult = { isGranted ->
isPermissionGranted = isGranted // Update state
// If denied, check if Rationale should be shown (state needs update even if system dialog is closed)
if (!isGranted) {
val activity = context.findActivity()
showRationaleDialog = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
}
)
// Request
permissionLauncher.launch(permission)
Starting with Android 11, the applicable method for checking and requesting changes, requiring navigation to the settings screen for authorization instead of using the launcher.
// Check storage permission
if(Environment.isExternalStorageManager()){
//...
}
// Navigate to settings screen for authorization
val packageName = activity.packageName
val intent = Intent()
intent.action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
intent.data = Uri.fromParts("package", packageName, null)
startActivity(activity, intent, null)
Summary
The details of permission management on Android can be quite complex. We hope the template provided in this article is helpful to everyone.
Top comments (0)