搞定在Jetpack Compose中优雅地申请运行时权限

随着安全与隐私问题越来越被人们所重视,操作系统对应用程序的限制也越来越严格。一个非常明显的控制就是对于运行时权限(Runtime permissions)的管控是越来越严格,很多原本不需要权限的地方也需要了权限。这就要求应用程序必须能够灵活的处理运行时权限。Jetpack Compose作为一个独立于平台的声明式UI框架,本身并没有权限的概念,权限是平台强相关的,本文将研究一下如何在Compose中优雅的申请运行时权限。

注意: 这里提到的权限都是运行时权限,也就是需要在访问某些API之前动态地向用户申请授权许可。

运行时权限申请用例

有过Android开发经验的同学对运行时权限申请一定不陌生。自从Android Marshmallow(6.0,API Level 23)开始,对于一些敏感的权限,除了在应用的Manifest中声明以外,还需要在运行时动态的向用户申请使用权限,只有在用户同意授权后才可以使用相关的功能,当然用户也可能会拒绝。自此,运行时的权限申请就变成了应用开发的一个标配了。

比如以相机权限为例,第一步,要先在AndroidManifest中声明权限使用:

xml 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.CAMERA" />
    <application
        ...
    </application>
</manifest>

之后第二步,在要使用相机的入口地方,也就是要访问相机API的入口处先进行权限检查,如果已授予,则走正常的逻辑(如打开相机),否则进行权限申请:

Kotlin 复制代码
    // 调用相机的入口处
    if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
        happyWithCamera()
    } else {
        // 提示用户需要权限,然后申请权限
        ActivityCompat.requestPermissions(context,  arrayOf(Manifest.permission.CAMERA), 1024)
    }

权限申请会离开当前的页面,进入到系统的权限处理逻辑。然后第三步,在系统权限回调中(类似于onActivityResult)检查用户权限授予结果,如果已授予则走第二步中的正常逻辑,否则,弹窗提示用户,解释权限对于应用程序的必要性,视权限的必要程度和交互逻辑,可以再次申请权限或者走无权限的逻辑(假如部分功能还可用)或者直接退出:

Kotlin 复制代码
// 系统权限回调,类似于onActivityResult
@Override
public void onRequestPermissionResult(int reqCode, @NonNull String[] perms, @NonNull int[] results) {
    super.onRequestPermissionResult(reqCode, perms, results)
    if (reqCode == 1024) {
        if (results.length > 1 && results[0] == PackageManager.PERMISSION_GRANTED) {
            // 用户授权了,可以使用相机了
            happyWithCamera()
        } else {
            // 权限被用户拒绝,有三种做法:
            // 1. 如果是必须的权限,可再次申请;
            // 2. 如果已被拒绝多次,或者不想再次申请,那就提示用户然后退出
            // 3. 如果是非必须权限,那就走剩余的流程
        }
    }
}

如果是多个权限,处理的方式也是一样的,因为申请权限以及权限回调中都是数组,也即是可以处理多个权限。

对于在何时着手处理权限,要视权限对业务逻辑的重要程度,如果是必须的权限(如Location于地图应用,Camera于相机应用)那应该在应用启动时,加载任何页面之前作为第一件事情去做;如果不是,非主要业务逻辑,如微信或者支付宝的扫码功能,绝大多数应用都有扫码功能但都非其主要业务逻辑,那应该在用户启动扫码功能时去处理相机权限。

以原生方式申请运行时权限

让我们回到Compose世界,因为在Jetpack Compose中没有权限的概念,因此要把原生的权限处理方式在Composable中完成。第一步权限的声明仍需要在应用程序的Manifest中来做,这一步是没有变化的。

第二步和第三步有些麻烦,Compose是由一坨坨的composable函数组成的,我们只能调用其他的函数,没有办法处理权限回调,这是Activity的一个public方法,不是可以设置的常规回调,因为我们没有办法创建Activity的实例。另外前面的例子中请求权限会用到Activity,而在Compossable中不应该去尝试获取Activity,虽然是可以拿到实例的,但作为一个独立的UI框架不应该去拿平台强相关的且生命周期不可控的对象实例。

这里就可以通过一个叫做ActivityResultLancher来对Activity的跳转和onActivityResult进行封装,创建一个ActivityResultLancher实例,设置一个onResult回调来处理onActivityResult,这个launcher也可以用来启动新的Activity。本质上与覆写Activity的方法也是一样的,但最重要的是我们可以自主的创建ActivityResultLancher对象,这样就可以在纯函数式的composable中使用了。ActivityResultLancher可以应对很多跳转场景,由ActivityResultContract对象来定义,已经有很多预定义类型了,对于申请权限要使用RequestPermission

仍是以相机权限为例,假定是一个相机权限强必须的拍照应用,可以在应用的入口处定义PermissionInterceptor来处理权限:

Kotlin 复制代码
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloComposeTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    PermissionInterceptor(
                        content = {
                            Text("Happy with Camera!")
                        },
                        noPermission = {
                            Text("To use this app, you must grant CAMERA permission!")
                        }
                    )
                }
            }
        }
    }
}

@SuppressLint("LaunchDuringComposition")
@Composable
fun PermissionInterceptor(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.()->Unit,
    noPermission: @Composable BoxScope.()->Unit
) {
    val context = LocalContext.current
    val permission = Manifest.permission.CAMERA

    var isGranted by remember { mutableStateOf<Boolean?>(null) }
    val permissionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        isGranted = granted
    }

    Box(modifier = modifier, contentAlignment = Alignment.Center) {
        if (isGranted == true ||
            ContextCompat.checkSelfPermission(
                context,
                permission
            ) == PackageManager.PERMISSION_GRANTED) {
            content()
        } else if (isGranted == null) {
            LaunchedEffect(permissionLauncher) {
                permissionLauncher.launch(permission)
            }
        } else {
            noPermission()
        }
    }
}

需要注意是如果是像上面例子这样在应用的入口处时就检查并申请权限,需要把launcher包裹在副作用函数LauncherEffect中,否则会有『IllegalStateException: Launcher has not been initialized』,这是因为初始化工作并未做完,但如果是用户点击之后才会触发权限申请则不需要。

在实际项目中,可能不止一个权限需要申请。申请多个动态权限,流程逻辑也一样的,需要传入RequestMultiplePermissions,以及参数和结果都是数组:

Kotlin 复制代码
@Composable
fun MultiplePermissionsInterceptor(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.()->Unit,
    noPermission: @Composable BoxScope.()->Unit
) {
    val context = LocalContext.current
    val permissions = arrayOf(
                        Manifest.permission.CAMERA,
                        Manifest.permission.ACCESS_COARSE_LOCATION,
                        Manifest.permission.ACCESS_FINE_LOCATION
                    )
    var isGranted by remember { mutableStateOf<Boolean?>(null) }
    val permissionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { granted ->
        isGranted = granted.all { it.value }
    }

    Box(modifier = modifier, contentAlignment = Alignment.Center) {
        if (isGranted == true ||
            permissions.all {
                ContextCompat.checkSelfPermission(
                    context,
                    it
                ) == PackageManager.PERMISSION_GRANTED
            }
        ) {
            content()
        } else if (isGranted == null) {
            LaunchedEffect(permissionLauncher) {
                permissionLauncher.launch(permissions)
            }
        } else {
            noPermission()
        }
    }
}

使用Accompanist-permissions

事实上谷歌也在着手解决Compose中的权限处理问题,在accompanist库中有处理权限的API。

Accompanist是一个由谷歌提供的Jetpack Compose的补充库,也就是说一些开发者强烈需求的API,但还未正式放入Compose中,但谷歌也有意要提供,那么就会先放放Accompanist中,等开发完成试用很好,可能就会移入到Jetpack Compose中变成正式的API。Accompanist中能找到很多新奇的东西,比如像下拉刷新,流式布局,权限处理,WebView等等。但需要注意的是Accompanist多半是试验性的,API很不稳定,说变就变,说没就没,使用之前要三思。

配置accompanist-permission

Accompanist是一个独立的库,而且拆分的很细,权限处理是一个独立的包:

Kotlin 复制代码
dependencies {
    // Accompanist permission
    val accompanistVersion = "0.32.0"
    implementation("com.google.accompanist:accompanist-permissions:$accompanistVersion")
}

使用Accompanist-permission

既然是API自然在封装上会做的更好,使用起来更加的方便。Accompanist使用rememberPermissionStaterememberMultiplePermissionsState返回一个状态PermissionState,这个状态既可以检查权限申请结果,也可以去申请权限,体验丝般顺滑:

Kotlin 复制代码
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun AccompanistInterceptor(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.()->Unit,
    noPermission: @Composable BoxScope.()->Unit
) {
    val permissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)

    Box(modifier = modifier, contentAlignment = Alignment.Center) {
        if (permissionState.status.isGranted) {
            content()
        } else if (permissionState.status.shouldShowRationale) {
            noPermission()
        } else {
            LaunchedEffect(permissionState) {
                permissionState.launchPermissionRequest()
            }
        }
    }
}

处理多个权限时用带有multiple字样的接口就可以了:

Kotlin 复制代码
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun MultipleAccompanistInterceptor(
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.()->Unit,
    noPermission: @Composable BoxScope.()->Unit
) {
    val permissions = listOf(
                        Manifest.permission.CAMERA,
                        Manifest.permission.ACCESS_COARSE_LOCATION,
                        Manifest.permission.ACCESS_FINE_LOCATION
                    )
    val permissionState = rememberMultiplePermissionsState(permissions = permissions)
    
    Box(modifier = modifier, contentAlignment = Alignment.Center) {
        if (permissionState.allPermissionsGranted) {
            content()
        } else if (permissionState.shouldShowRationale) {
            noPermission()
        } else {
            LaunchedEffect(permissionState) {
                permissionState.launchMultiplePermissionRequest()
            }
        }
    }
}

注意: 因为Accompanist库是实验性质的,所以它的API都要求带上注解@OptIn(ExperimentalPermissionsApi::class)。

扩展阅读:

总结

本文详细了介绍了目标平台是Android时,Compose的两种运行时权限申请方式。推荐使用Accompanist库中的permission模块,虽然这会引入一个新的依赖,虽然它只是实验性的,毕竟用起来方便啊。当然,如果想要精细化的处理权限的各种结果就直接用原生方式,也并没有麻烦多少。

References

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
花追雨7 小时前
Android -- 双屏异显之方法一
android·双屏异显
小趴菜82277 小时前
安卓 自定义矢量图片控件 - 支持属性修改矢量图路径颜色
android
氤氲息7 小时前
Android v4和v7冲突
android
KdanMin7 小时前
高通Android 12 Launcher应用名称太长显示完整
android
chenjk47 小时前
Android不可擦除分区写文件恢复出厂设置,无法读写问题
android
袁震7 小时前
Android-Glide缓存机制
android·缓存·移动开发·glide
工程师老罗7 小时前
Android笔试面试题AI答之SQLite(2)
android·jvm·sqlite
User_undefined9 小时前
uniapp Native.js 调用安卓arr原生service
android·javascript·uni-app
安小牛9 小时前
android anr 处理
android
刘争Stanley12 小时前
如何高效调试复杂布局?Layout Inspector 的 Toggle Deep Inspect 完全解析
android·kotlin·android 15·黑屏闪屏白屏