Android에서 권한을 요청하는 것이 결코 쉬운 일이 아닌 이유는 무엇입니까?

주말에 둥관과 선전에서 2개의 GDG에 참가했는데 모두 온라인이라 서두르지 않고 집에 앉아서 행사가 시작되기를 기다려야 했습니다.

기다리는 시간이 변덕스럽고 갑자기 Android 권한 요청 코드를 작성할 때 내 자신의 기술적 경험에 대해 이야기하는 원본 기사를 작성하고 싶습니다.

이 기사의 제목에서 설명하는 것처럼 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 = true
                for (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 = true
                for (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 = uri
                                startActivityForResult(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 开源库地址:github.com/guolindev/P…

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

关注我的技术公众号“郭霖”,每周都有优质技术文章推送。

本文是去年所写,现搬运到掘金上来。

추천

출처juejin.im/post/6981718589598498824