私は週末に東莞と深センの2つのGDGに参加しましたが、それらはすべてオンラインだったので、急いでいませんでした。家に座ってイベントが始まるのを待つだけでした。
待ち時間は気まぐれで、突然、Androidの許可リクエストコードを書くときの自分の技術的経験について話すオリジナルの記事を書きたいと思います。
この記事のタイトルで説明されているように、Androidで権限をリクエストすることは決して簡単な作業ではありませんでした。なんで?Googleは、ランタイム権限機能を設計する際にユーザーエクスペリエンスを十分に考慮していると思いますが、開発者のコーディングエクスペリエンスを十分に考慮していません。
公式アカウントのコメント欄で話し合ったところ、友達が言った:Androidが提供するランタイムパーミッションAPIはとても便利だと思うし、使うのも面倒ではないと思う。
一
本当か?具体的な例を見てみましょう。
カメラ機能を開発しているとしましょう。カメラ機能には通常、カメラ許可と測位許可が必要です。つまり、カメラ機能を実現するには、この2つの許可が前提条件です。ユーザーは、この2つの許可に同意してからでないと、続行できます。写真を撮ります。
では、これら2つの権限を申請するにはどうすればよいでしょうか。誰もが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()メソッドで認証結果が監視されていることがわかります。ユーザーがこれらの2つの権限に同意すると、写真を撮ることができます。ユーザーがいずれかの権限を拒否すると、トーストプロンプトがポップアップして、特定の権限が拒否されたことをユーザーに通知し、カメラを撮影できなくなります。 。
この書き方は面倒ですか?慈悲深い人は慈悲深い人を見て、賢い人は知恵を見て、何人かの友人はコードの行があまりないと思うかもしれません、それで問題は何ですか。しかし、個人的にはかなり面倒だと思います。実行時のアクセス許可を要求する必要があるたびに、私は非常に疲れて、そのような長いコードを書きたくありません。
ただ、とりあえずシンプルさの観点からは考えませんが、正しさの観点から、この書き方は正しいのでしょうか?トーストをポップアップして許可が拒否されたときにユーザーに通知し、フォローアップ操作計画を提供しないため、問題があると思います。ユーザーが本当に許可を拒否した場合、アプリケーションは引き続き使用できません。 。
因此,我们还需要提供一种机制,当权限被用户拒绝时,可以再次重新请求权限。
现在我对代码进行如下修改:
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 也非常简单,只需要添加如下的依赖即可:
dependencies {
...
implementation 'com.permissionx.guolindev:permissionx:1.3.1'
}
复制代码
最后附上 PermissionX 开源库地址:github.com/guolindev/P…
如果想要学习 Kotlin 和最新的 Android 知识,可以参考我的新书 《第一行代码 第 3 版》,点击此处查看详情。
关注我的技术公众号“郭霖”,每周都有优质技术文章推送。
本文是去年所写,现搬运到掘金上来。