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

周末时间参加了东莞和深圳的两场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()}}

可以看到,这里先是通过调用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()}}

这里我将请求权限的代码提取到了一个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()}}

现在代码已经变得比较长了,我还是带着大家来梳理一下。

这里我在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()}}

可以看到,请求权限的代码一下子变得极其精简。

我们只需要在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'
}

最后附上PermissionX开源库地址:https://github.com/guolindev/PermissionX

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

关注我的技术公众号,每天都有优质技术文章推送。

微信扫一扫下方二维码即可关注:

为什么说在Android中请求权限从来都不是一件简单的事情?相关推荐

  1. Android中各个权限详解

    在Android的设计中,资源的访问或者网络连接,要得到这些服务都需要声明其访问权限,否则将无法正常工作.在Android中这样的权限有很多种,这里将各类访问权限一一罗列出来,供大家使用时参考之用. ...

  2. Android 中的权限

    1.权限类型 Android 将权限分为不同的类型,包括安装时权限.运行时权限和特殊权限.每种权限类型都指明了当系统授予应用该权限后,应用可以访问的受限数据范围以及应用可以执行的受限操作范围.每项权限 ...

  3. Android中的权限-中英对照

    <uses-permission android:name="android.permission.ACCESS_CHECKIN_PROPERTIES" ></u ...

  4. Android中SeLinux权限 .te文件编写

    在android中添加一个LocalSocket通信,权限部分折腾了好几天,终于搞定了. 首先在root权限下使用setenforce 0命令放开selinux权限,看看需要的操作是否能成功.如果可以 ...

  5. android 原生请求权限代码

    每次开新项目都要到处找请求权限的代码,老火得很,这里记录下,方便以后搬运,当然也有很多请求权限的开源项目, @Overrideprotected void onCreate(Bundle savedI ...

  6. android动态请求权限

    /*** 动态请求权限*/ private void requestPermission() {//动态请求权限if (Build.VERSION.SDK_INT >= Build.VERSIO ...

  7. Android中读写权限申请

    1. 在AndroidManifest中的操作 自Android 6.0开始,Google开始对系统权限做出严格的要求,有些权限必须用户同意才能调用相应功能,所以开发者需要调用权限申请的代码,弹出一个 ...

  8. Android ActivityResultContracts 请求权限(封装;含android 11权限变更)

    文章目录 关联博客 Android 11 权限变更 权限申请 BasePermissionExtendFragment.kt PermissionExtend.kt 关联博客 Android Acti ...

  9. android组件权限,Android中Permission权限机制的具体使用

    由上篇Android Permission权限机制引子,我们知道Android 通过在每台设备上实施了基于权限的安全策略来处理安全问题,采用权限来限制安装应用程序的能力.本篇文章继续来探讨和Andro ...

最新文章

  1. mysql图形化及命令行操作用户权限
  2. [转载] 晓说——第16期:古代科举那些事——由来
  3. Oracle查看用户权限
  4. redis高可用原理及demo
  5. Visual C++——黄维通《 Visual C++面向对象与可视化程序设计》——习题7-12
  6. boost::fibers::barrier用法的测试程序
  7. pyecharts第四节、漏斗图
  8. CodeForces 696B Puzzles
  9. MFC创建模式对话框与非模式对话框
  10. dedecms织梦仿麦站网模板源码下载站源码
  11. 小红书回应行政处罚:因12月央视报道提及未成年信息审核漏放
  12. 接口测试流程及常见问答
  13. C语言实现线性表的链式存储结构
  14. matlab实现灰度图像伪彩色处理
  15. QCC3040---UI tones module
  16. SK创新在2019年下半年将实现柔性显示器核心材料FCW量产
  17. 黑吃黑第一季/全集Banshee迅雷下载
  18. CIO烦恼之七:企业文化僵化,系统思想难以贯彻
  19. mysql cursor使用变量_mysql cursor游标的使用,实例
  20. install firebox on ubuntu

热门文章

  1. cadence16.6软件窗口内容缺失
  2. 极域电子教室6.0全屏变窗口_Cookie for mac(浏览器痕迹清理工具) 6.0.1
  3. Mitmproxy详细教程及二级代理
  4. i18n和i10n:国际化本地化--gettext
  5. HC-CB01数字光纤同轴模块说明
  6. Vue 前端代码风格指南
  7. 小米游戏本0909bios_小米有品上架雷蛇灵刃17游戏本,最贵3.4万元
  8. 《走近ZStack Mini》第三期:智慧医疗场景演示
  9. java 单元测试用例_Java 单元测试及JUnit的使用
  10. java中所有类都是通过_Java中所有的类都是通过直接或间接地继承(   )类得到的...