为什么说在Android中请求权限从来都不是一件简单的事情?(转载)

2023-12-11 13:58

本文主要是介绍为什么说在Android中请求权限从来都不是一件简单的事情?(转载),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

周末时间参加了东莞和深圳的两场GDG,因为都是线上参与,所以时间上并不赶,我只需要坐在家里等活动开始就行了。

等待的时间一时兴起,突然想写一篇原创,聊一聊我自己在写Android权限请求代码时的一些技术心得。

正如这篇文章标题所描述的一样,在Android中请求权限从来都不是一件简单的事情。为什么?我认为Google在设计运行时权限这块功能时,充分考虑了用户的使用体验,但是却没能充分考虑开发者的编码体验。

之前在公众号的留言区和大家讨论时,有朋友说:我觉得Android提供的运行时权限API很好用呀,并没有觉得哪里使用起来麻烦。

真的是这样吗?我们来看一个具体的例子。

假设我正在开发一个拍照功能,拍照功能通常都需要用到相机权限和定位权限,也就是说,这两个权限是我实现拍照功能的先决条件,一定要用户同意了这两个权限我才能继续进行拍照。

那么怎样去申请这两个权限呢?Android提供的运行时权限API相信每个人都很熟悉了,我们自然而然可以写出如下代码:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1)
}override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)when (requestCode) {1 -> {var allGranted = truefor (result in grantResults) {if (result != PackageManager.PERMISSION_GRANTED) {allGranted = false}}if (allGranted) {takePicture()} else {Toast.makeText(this, "您拒绝了某项权限,无法进行拍照", Toast.LENGTH_SHORT).show()}}}
}fun takePicture() {Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show()
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
可以看到,这里先是通过调用requestPermissions()方法请求相机权限和定位权限,然后在onRequestPermissionsResult()方法里监听授权的结果。如果用户同意了这两个权限,那么我们就可以去进行拍照了,如果用户拒绝了任意一个权限,那么弹出一个Toast提示,告诉用户某项权限被拒绝了,从而无法进行拍照。

这种写法麻烦吗?这个就仁者见仁智者见智了,有些朋友可能觉得这也没多少行代码呀,有什么麻烦的。但我个人认为还是比较麻烦的,每次需要请求运行时权限时,我都会觉得很心累,不想写这么啰嗦的代码。

不过我们暂时不从简易性的角度考虑,从正确性的角度上来讲,这种写法对吗?我认为是有问题的,因为我们在权限被拒绝时只是弹了一个Toast来提醒用户,并没有提供后续的操作方案,用户如果真的拒绝了某个权限,应用程序就无法继续使用了。

因此,我们还需要提供一种机制,当权限被用户拒绝时,可以再次重新请求权限。

现在我对代码进行如下修改:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)requestPermissions()
}override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)when (requestCode) {1 -> {var allGranted = truefor (result in grantResults) {if (result != PackageManager.PERMISSION_GRANTED) {allGranted = false}}if (allGranted) {takePicture()} else {AlertDialog.Builder(this).apply {setMessage("拍照功能需要您同意相机和定位权限")setCancelable(false)setPositiveButton("确定") { _, _ ->requestPermissions()}}.show()}}}
}fun requestPermissions() {ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1)
}fun takePicture() {Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show()
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
这里我将请求权限的代码提取到了一个requestPermissions()方法当中,然后在onRequestPermissionsResult()里判断,如果用户拒绝了某项权限,那么就弹出一个对话框,告诉用户相机和定位权限是必须的,然后在setPositiveButton的点击事件中调用requestPermissions()方法重新请求权限。

我们来看一下现在的运行效果:

可以看到,现在我们对权限被拒绝的场景进行了更加充分的考虑。

那么现在这种写法,是不是就将请求运行时权限的各种场景都考虑周全了呢?其实还没有,因为Android权限系统还提供了一种非常“恶心”的机制,叫拒绝并不再询问。

当某个权限被用户拒绝了一次,下次我们如果再申请这个权限的话,界面上会多出一个拒绝并不再询问的选项。只要用户选择了这一项,那么完了,我们之后都不能再去请求这个权限了,因为系统会直接返回我们权限被拒绝。

这种机制对于用户来说非常友好,因为它可以防止一些恶意软件流氓式地无限重复申请权限,从而严重骚扰用户。但是对于开发者来说,却让我们苦不堪言,如果我的某项功能就是必须依赖于这个权限才能运行,现在用户把它拒绝并不再询问了,我该怎么办?

当然,绝大多数的用户都不是傻X,当然知道拍照功能需要用到相机权限了,相信99%的用户都会点击同意授权。但是我们可以不考虑那剩下1%的用户吗?不可以,因为你们公司的测试就是那1%的用户,他们会进行这种傻X式的操作。

也就是说,即使只为了那1%的用户,为了这种不太可能会出现的操作方式,我们在程序中还是得要将这种场景充分考虑进去。

那么,权限被拒绝且不再询问了,我们该如何处理呢?比较通用的处理方式就是提醒用户手动去设置当中打开权限,如果想做得再好一点,可以提供一个自动跳转到当前应用程序设置界面的功能。

下面我们就来针对这种场景进行完善,如下所示:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)requestPermissions()
}override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)when (requestCode) {1 -> {val denied = ArrayList<String>()val deniedAndNeverAskAgain = ArrayList<String>()grantResults.forEachIndexed { index, result ->if (result != PackageManager.PERMISSION_GRANTED) {if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[index])) {denied.add(permissions[index])} else {deniedAndNeverAskAgain.add(permissions[index])}}}if (denied.isEmpty() && deniedAndNeverAskAgain.isEmpty()) {takePicture()} else {if (denied.isNotEmpty()) {AlertDialog.Builder(this).apply {setMessage("拍照功能需要您同意相册和定位权限")setCancelable(false)setPositiveButton("确定") { _, _ ->requestPermissions()}}.show()} else {AlertDialog.Builder(this).apply {setMessage("您需要去设置当中同意相册和定位权限")setCancelable(false)setPositiveButton("确定") { _, _ ->val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)val uri = Uri.fromParts("package", packageName, null)intent.data = uristartActivityForResult(intent, 1)}}.show()}}}}
}override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)when (requestCode) {1 -> {requestPermissions()}}
}fun requestPermissions() {ActivityCompat.requestPermissions(this,arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION), 1)
}fun takePicture() {Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show()
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
现在代码已经变得比较长了,我还是带着大家来梳理一下。

这里我在onRequestPermissionsResult()方法中增加了denied和deniedAndNeverAskAgain两个集合,分别用于记录拒绝和拒绝并不再询问的权限。如果这两个集合都为空,那么说明所有权限都被授权了,这时就可以直接进行拍照了。

而如果denied集合不为空,则说明有权限被用户拒绝了,这时候我们还是弹出一个对话框来提醒用户,并重新申请权限。而如果deniedAndNeverAskAgain不为空,说明有权限被用户拒绝且不再询问,这时就只能提示用户去设置当中手动打开权限,我们编写了一个Intent来执行跳转逻辑,并在onActivityResult()方法,也就是用户从设置回来的时候重新申请权限。

那么现在运行一下程序,效果如下图所示:

可以看到,当我们第一次拒绝权限的时候,会提醒用户,相机和定位权限是必须的。而如果用户继续置之不理,选择拒绝并不再询问,那么我们将提醒用户,他必须手动开户这些权限才能继续运行程序。

到现在为止,我们才算是把一个“简单”的权限请求流程用比较完善的方式处理完毕。然而代码写到这里真的还算是简单吗?每次申请运行时权限,都要写这么长长的一段代码,你真的受得了吗?

这也就是我编写PermissionX这个开源库的原因,在Android中请求权限从来都不是一件简单的事情,但它不应该如此复杂。

PermissionX将请求运行时权限时那些应该考虑的复杂逻辑都封装到了内部,只暴露最简单的接口给开发者,从而让大家不需要考虑上面我所讨论的那么多场景。

而我们使用PermissionX来实现和上述一模一样的功能,只需要这样写就可以了:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)PermissionX.init(this).permissions(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION).onExplainRequestReason { scope, deniedList ->val message = "拍照功能需要您同意相册和定位权限"val ok = "确定"scope.showRequestReasonDialog(deniedList, message, ok)}.onForwardToSettings { scope, deniedList ->val message = "您需要去设置当中同意相册和定位权限"val ok = "确定"scope.showForwardToSettingsDialog(deniedList, message, ok)}.request { _, _, _ ->takePicture()}
}fun takePicture() {Toast.makeText(this, "开始拍照", Toast.LENGTH_SHORT).show()
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
可以看到,请求权限的代码一下子变得极其精简。

我们只需要在permissions()方法中传入要请求的权限名,在onExplainRequestReason()和onForwardToSettings()回调中填写对话框上的提示信息,然后在request()回调中即可保证已经得到了所有请求权限的授权,调用takePicture()方法开始拍照即可。

通过这样的直观对比大家应该能感受到PermissionX所带来的便利了吧?上面那段长长的请求权限的代码我真的是为了给大家演示才写的,而我再也不想写第二遍了。

另外,本篇文章主要只是演示了一下PermissionX的易用性,并不涉及其中具体的诸多用法,如Android 11兼容性,自定义对话框样式等等。如果大家感兴趣的话,更多用法请参考下面的链接。

Android运行时权限终极方案,用PermissionX吧

PermissionX现在支持Java了!还有Android 11权限变更讲解

PermissionX重磅更新,支持自定义权限提醒对话框

在项目中引入PermissionX也非常简单,只需要添加如下的依赖即可:

dependencies {

implementation ‘com.permissionx.guolindev:permissionx:1.3.1’
}
1
2
3
4
最后附上PermissionX开源库地址:https://github.com/guolindev/PermissionX

如果想要学习Kotlin和最新的Android知识,可以参考我的新书 《第一行代码 第3版》,点击此处查看详情。

原文链接:https://blog.csdn.net/guolin_blog/article/details/107590047?utm_medium=distribute.pc_feed.none-task-blog-alirec-11.nonecase&depth_1-utm_source=distribute.pc_feed.none-task-blog-alirec-11.nonecase

这篇关于为什么说在Android中请求权限从来都不是一件简单的事情?(转载)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/480929

相关文章

Java中使用 @Builder 注解的简单示例

《Java中使用@Builder注解的简单示例》@Builder简化构建但存在复杂性,需配合其他注解,导致可变性、抽象类型处理难题,链式编程非最佳实践,适合长期对象,避免与@Data混用,改用@G... 目录一、案例二、不足之处大多数同学使用 @Builder 无非就是为了链式编程,然而 @Builder

nginx中端口无权限的问题解决

《nginx中端口无权限的问题解决》当Nginx日志报错bind()to80failed(13:Permissiondenied)时,这通常是由于权限不足导致Nginx无法绑定到80端口,下面就来... 目录一、问题原因分析二、解决方案1. 以 root 权限运行 Nginx(不推荐)2. 为 Nginx

Android kotlin中 Channel 和 Flow 的区别和选择使用场景分析

《Androidkotlin中Channel和Flow的区别和选择使用场景分析》Kotlin协程中,Flow是冷数据流,按需触发,适合响应式数据处理;Channel是热数据流,持续发送,支持... 目录一、基本概念界定FlowChannel二、核心特性对比数据生产触发条件生产与消费的关系背压处理机制生命周期

Android ClassLoader加载机制详解

《AndroidClassLoader加载机制详解》Android的ClassLoader负责加载.dex文件,基于双亲委派模型,支持热修复和插件化,需注意类冲突、内存泄漏和兼容性问题,本文给大家介... 目录一、ClassLoader概述1.1 类加载的基本概念1.2 android与Java Class

Android DataBinding 与 MVVM使用详解

《AndroidDataBinding与MVVM使用详解》本文介绍AndroidDataBinding库,其通过绑定UI组件与数据源实现自动更新,支持双向绑定和逻辑运算,减少模板代码,结合MV... 目录一、DataBinding 核心概念二、配置与基础使用1. 启用 DataBinding 2. 基础布局

Android ViewBinding使用流程

《AndroidViewBinding使用流程》AndroidViewBinding是Jetpack组件,替代findViewById,提供类型安全、空安全和编译时检查,代码简洁且性能优化,相比Da... 目录一、核心概念二、ViewBinding优点三、使用流程1. 启用 ViewBinding (模块级

基于Python实现一个简单的题库与在线考试系统

《基于Python实现一个简单的题库与在线考试系统》在当今信息化教育时代,在线学习与考试系统已成为教育技术领域的重要组成部分,本文就来介绍一下如何使用Python和PyQt5框架开发一个名为白泽题库系... 目录概述功能特点界面展示系统架构设计类结构图Excel题库填写格式模板题库题目填写格式表核心数据结构

如何搭建并配置HTTPD文件服务及访问权限控制

《如何搭建并配置HTTPD文件服务及访问权限控制》:本文主要介绍如何搭建并配置HTTPD文件服务及访问权限控制的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、安装HTTPD服务二、HTTPD服务目录结构三、配置修改四、服务启动五、基于用户访问权限控制六、

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

C/C++ chrono简单使用场景示例详解

《C/C++chrono简单使用场景示例详解》:本文主要介绍C/C++chrono简单使用场景示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友... 目录chrono使用场景举例1 输出格式化字符串chrono使用场景China编程举例1 输出格式化字符串示