使用 Compose 权限请求模板高效搭建应用权限流程

使用 Compose 权限请求模板高效搭建应用权限流程

前言

在传统的 Android 开发中,权限请求依赖于 Activity 中的 requestPermissions()onRequestPermissionsResult() 方法,权限请求逻辑往往集中写在 Activity 中,基于回调的模式的代码结构也显得比较琐碎 随着 Jetpack Compose 的崛起,我们需要一种更符合声明式 UI 范式的权限处理方式。官方推荐的 Activity Result API (通过 rememberLauncherForActivityResult)将请求和结果的获取解耦,让我们的 Compose 函数能够以 状态驱动 的方式处理权限。

在 Jetpack Compose 中,权限处理虽然遵循声明式原则,但每次为一个新权限编写 权限检查 → 启动器注册 → Rationale 判断 → 永久拒绝处理 的全套流程依然耗时且重复。

为高效开发,本文目标是将这套流程模板化、封装化 。本文将提供一个 高度可复用、符合官方最佳实践 的 Compose 权限请求通用模板,帮助开发者在任何应用中快速、优雅地搭建权限申请流程。

本文目标

目标 重点内容 解决痛点
通用模板化 封装权限请求、状态、理由展示和永久拒绝处理为 一个可复用的 Composable 避免重复编写大量权限检查和回调逻辑。
高可复用性 设计参数灵活的函数,只需传入权限名称和授予后的回调,即可使用。 适用于应用中所有危险权限的请求场景。
状态驱动设计 内部实现遵循 Compose State 原理,确保 UI 响应灵敏且状态准确。 保证权限 UI 与应用状态始终同步,避免 Bug。
官方推荐内核 模板底层基于 Activity Result APIshouldShowRationale 最佳实践。 保证代码的健壮性和前瞻性。

基础准备:Manifest 声明与依赖引入

无论采用哪种方式,首先必须在 AndroidManifest.xml 文件中声明所有需要的权限(Dangerous Permissions)。

xml 复制代码
<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>

引入 Compose Activity 依赖

确保您的项目中包含用于 Compose 的 Activity Result API 依赖:

groovy 复制代码
// build.gradle (app level)
dependencies {
    // 必须引入,它提供了 rememberLauncherForActivityResult 等核心 API
    implementation("androidx.activity:activity-compose:1.9.0") // 请使用最新版本
    
    // 如果您需要处理多个权限,可以使用此 Contract
    // implementation("androidx.activity:activity-ktx:1.9.0")
}

核心实践:使用 rememberLauncherForActivityResult构建权限请求模板

在 Compose 中请求权限的核心是使用 rememberLauncherForActivityResult 。它是一个 Compose 可组合函数,负责注册一个 ActivityResultLauncher,并在组件被移除时自动注销。我们基于此API构建自己的权限请求模板。

Activity Context 获取工具

由于权限请求中关键的 shouldShowRationale API 依赖于 Activity,我们首先定义一个实用的 Context 扩展函数来获取它。

kotlin 复制代码
// 放在您的工具类中,方便全局调用
fun Context.findActivity(): Activity {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    // 权限请求必须在 Activity Context 中进行
    throw IllegalStateException("Permissions must be requested within an Activity Context.")
}

模板代码实现

我们将所有的权限处理逻辑封装到一个名为 PermissionRequestTemplate 的 Composable 函数中。它只需要三个核心参数:

  1. permission: 要请求的权限字符串。
  2. content: 权限授予后要展示的核心功能 UI。
  3. rationaleContent: 解释权限理由时要展示的 UI。
kotlin 复制代码
@Composable
fun PermissionRequestTemplate(
    permission: String,
    text:String= stringResource(id = R.string.request_permission),
   // content: @Composable () -> Unit, // 权限授予后显示的内容
    rationaleContent: @Composable (
        // 解释理由后,重新发起请求的Action
        onRequestPermission: () -> Unit,
        // 永久拒绝后,跳转到设置页的Action
        onOpenSettings: () -> Unit
    ) -> Unit,
   // 永久拒绝后,跳转到设置页的Action
    onOpenSettings: () -> Unit
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    // ① 核心状态:跟踪权限是否已授予
    var isPermissionGranted by remember {
        mutableStateOf(ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED)
    }
    // 核心状态:跟踪是否需要展示"理由解释"UI
    var showRationaleDialog by remember { mutableStateOf(false) }
    var showPermissionPromptDialog by remember { mutableStateOf(true) }
    // ② 注册权限请求启动器
    val permissionLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
        onResult = { isGranted ->
            isPermissionGranted = isGranted // 更新状态
            // 如果被拒绝,检查是否需要弹出 Rationale(尽管系统对话框已关闭,但状态仍需更新)
            if (!isGranted) {
                val activity = context.findActivity()
                showRationaleDialog = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
            }
        }
    )

    // ③ 权限状态检查与 UI 渲染
    when {
        // 状态 1: 权限已授予 -> 显示核心功能
        isPermissionGranted -> {
            //content()
        }

        // 状态 2: 需要展示 Rationale -> 显示理由解释 UI
        showRationaleDialog -> {
            RationaleDisplay(
                rationaleContent = rationaleContent,
                onDismiss = { showRationaleDialog = false },
                onRequest = {
                    // 重新发起请求
                    permissionLauncher.launch(permission)
                    showRationaleDialog = false // 请求发起后隐藏对话框
                },
                onOpenSettings=onOpenSettings
            )
        }

        // 状态 3: 初始或拒绝状态 -> 显示请求按钮/提示
        else -> {
            // 首次请求,或用户已永久拒绝(shouldShowRationale = false)
            if(showPermissionPromptDialog){
                AlertDialog(
                    onDismissRequest = {showPermissionPromptDialog=false},
                    title = {
                        Text(text)
                    },
                    text = {

                    },
                    confirmButton = {
                        TextButton(onClick = {
                            val activity = context.findActivity()
                            // 再次检查是否需要显示理由
                            if (ActivityCompat.shouldShowRequestPermissionRationale(
                                    activity,
                                    permission
                                )
                            ) {
                                showRationaleDialog = true
                            } else {
                                // 首次请求,或用户已永久拒绝,直接启动请求
                                permissionLauncher.launch(permission)
                            }
                        }) {
                            Text(stringResource(id = R.string.ok))
                        }
                    },
                    dismissButton = {
                        TextButton(onClick = {
                            showPermissionPromptDialog=false
                        }) {
                            Text(stringResource(id = R.string.cancel))
                        }
                    }
                )
            }
        }
    }
}

辅助 Composable 封装(按钮与理由)

为了让 PermissionRequestTemplate 保持简洁,将请求按钮和理由展示进行封装。

kotlin 复制代码
@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
                onOpenSettings = onOpenSettings // 跳转设置的 Action
            )
        },
        confirmButton = {
        },
        dismissButton = {
              TextButton(onClick = {onDismiss()}) {
                  Text(text = stringResource(id = R.string.cancel))
              }
        }
    )
}

模板的使用:简洁高效的调用示例

使用这个模板,使业务 Composable 将变得更简洁和符合声明式风格。

kotlin 复制代码
        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 {
                        // 引导用户再次请求
                        Button(onClick = onRequest) { Text(stringResource(id = R.string.reauthorization)) }
                        Spacer(modifier = Modifier.width(8.dp))
                        // 引导用户进入设置页(处理永久拒绝场景)
                        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)
            }
        )

通过 PermissionRequestTemplate 及其辅助 Composable,我们实现了 Compose 权限请求的目标:流程标准化、逻辑集中化、代码最小化。

不同版本的差异

不过很遗憾的是,Android上权限在不同版本的系统上权限的判断方法和申请方法是存在差异的,这就使得开发者其实不能拿着基于rememberLauncherForActivityResult的代码梭哈所有权限请求逻辑,不同版本代码写法是有差异的。

以存储权限为例子(谷歌官方并不是很推荐应用拿到存储空间访问权限,而是推荐使用它们提供的文件选择器)在Android 11之前,存储权限可以分为Manifest.permission.WRITE_EXTERNAL_STORAGE写权限和Manifest.permission.READ_EXTERNAL_STORAGE读权限两部分,直接使用checkSelfPermissionrememberLauncherForActivityResult方法判断就可以了。

kotlin 复制代码
//判断权限
if((ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED)){
    //...
}

//注册权限请求启动器
val permissionLauncher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.RequestPermission(),
    onResult = { isGranted ->
        isPermissionGranted = isGranted // 更新状态
        // 如果被拒绝,检查是否需要弹出 Rationale(尽管系统对话框已关闭,但状态仍需更新)
        if (!isGranted) {
            val activity = context.findActivity()
            showRationaleDialog = ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
        }
    }
)

//请求
permissionLauncher.launch(permission)

在Android 11之后,判断和请求的适用方法就变了,需要跳转到设置界面授权,而不是启动器授权

kotlin 复制代码
//判断存储权限
if(Environment.isExternalStorageManager()){
    //...
}

//跳转到设置界面授权
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)

总结

Android上权限管理的细节还是比较琐碎的,希望本文提供的模板对大家有帮助。

参考

1.请求运行时权限谷歌官方教程

2.演示隐私权最佳实践的 Codelab

相关推荐
H1004 天前
SharedFlow和StateFlow的方案选择-屏幕旋转设计
android jetpack
alexhilton5 天前
理解retain{}的内部机制:Jetpack Compose中基于作用域的状态保存
android·kotlin·android jetpack
Coffeeee5 天前
Labubu很难买?那是因为还没有用Compose来画一个
前端·kotlin·android jetpack
我命由我123457 天前
Android 对话框 - 对话框全屏显示(设置 Window 属性、使用自定义样式、继承 DialogFragment 实现、继承 Dialog 实现)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Jeled7 天前
Android 本地存储方案深度解析:SharedPreferences、DataStore、MMKV 全面对比
android·前端·缓存·kotlin·android studio·android jetpack
我命由我123458 天前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
alexhilton13 天前
Kotlin互斥锁(Mutex):协程的线程安全守护神
android·kotlin·android jetpack
是六一啊i14 天前
Compose 在Row、Column上使用focusRestorer修饰符失效原因
android jetpack