前段时间开始阅读郭霖大神的《第一行代码》,跟着书上的代码开发了一款天气App。因为认为手动获取地址实在不够方便,于是开始尝试写一个自动获取定位的feature。
随着现在用户对于隐私的重视不断提升,Android也在 6.0(M/API23) 开始对应用的权限进行了进一步的规范。从以前在安装时告知用户应用将会使用的权限,到如今运行时权限。对于用户来说这是个好消息,但是对于开发者来说,这意味着需要针对不同的权限向用户进行申请,这自然是一个头疼的问题。
这篇文章就以我自己对应用的一个需求 —— 获取“模糊定位”权限 为例,向大家展示一下如何让申请权限更加符合用户的逻辑,更加符合开发要求,更加符合Android开发规范。
关于当前Android的权限分类,请参考 Android Developer Guides
对于权限的申请,我们也需要思考是否做到了权限需求最小化,具体可以参考 Android开发团队的建议
提示:此文仅讲述如何更好的申请权限,关于权限的详细介绍,还请各位移步 这篇文章 进行阅读
STEP0:梳理 权限使用/申请 的流程
根据Android Developer Guides的这篇文章,我们可以发现请求权限的全流程主要分为以下几个步骤:
- 判断在没有权限的情况下是否能提供功能。如果可以,跳至5
- 判断是否声明权限
- 判断权限是否为运行时权限 (Runtime Permission)。如果不是,跳至5
- 向用户发出权限申请
- 完成权限申请流程。继续使用应用功能 或 无法提供对应功能
虽然从流程上看起来,应用只有一次向用户获取权限的机会。但是Android其实还是给予了开发者机会再次向用户解释权限用途并再次申请权限的机会。
我们可以通过Activity.shouldShowRequestPermission
方法判断用户是否已选择 “拒绝并不再询问” 选项。当返回值为true
时,我们可以使用Toast
/Snackbar
/Dialog
向用户解释权限用途,并引导用户再次授予权限。
STEP1:在Manifest中注册权限
因为对于天气应用而言,并不需要过于精确的地理信息。对于用户而言,通过粗略的坐标获取天气信息已经足够,所以考虑在注册权限时使用android.permission.ACCESS_COARSE_LOCATION
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
...
</manifest>
注意,如果在编写完代码后Build&Run时发现申请权限闪退,可以检查一下是否忘记申明权限。当然,如果观察Logcat,系统也会打印error信息并提示去Manifest声明权限。 (不会吧不会真的有人忘记吧)
STEP2:判断应用是否拥有权限
在整体的流程中,我们先需要考虑用户是否已经授予应用对应的权限。当我们获得权限时,则直接执行对应的业务逻辑(在本例中是获取定位);若没有获取用户授予的权限,则进行权限授权流程。
// set a button to use auto position feature
val getLocationBtn: Button = findViewById(R.id.request_location_btn)
// set button listener to determine whether user granted the permission
getLocationBtn.setOnClickListener {
// check permission here
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
// permission granted -> do get position feature here
doGetPosition()
} else {
// permission denied -> use this to request location permission,
// and we will expaine this later in artical
permissionRequestLauncher.launch("android.permission.ACCESS_COARSE_LOCATION")
}
}
这里使用到了一个permissionRequestLauncher
变量。很明显,我们在此处使用了它的lunch
方法,并传入了对应的 权限名称(String) 来进行权限的申请,关于它的具体介绍,下面就将提到。
STEP3:使用Acitivity Result API申请权限
在之前,我们请求权限需要使用ActivityCompat.requestPermission
方法来申请权限,并手动重写onRequestPermissionResult
方法完成请求回调。而在请求和回调之间需要使用requestCode
来保持请求和回调处理一一对应。
// this is java code, not kotlin
// source url here: https://zhuanlan.zhihu.com/p/76599492
private void callPermission(){
if(ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
} else {
callPhone();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == 1){
if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
callPhone();
}else {
Toast.makeText(this, "权限未授权!", Toast.LENGTH_SHORT).show();
}
}
}
很明显,这样的代码非常混乱、不清晰,而且如果在请求权限较少的时候,这样的代码还会显得非常不简洁。这时候不妨考虑一下使用Activity Result API
来解决这个问题。
我们首先需要声明一个ActivityResultLauncher<I>
变量。在API中,此泛型用于表示输入进去用于创建Intent的类型,此处我使用String类型,传入android.permission.ACCESS_COARSE_LOCATION
。 (见STEP2,具体原因后面讲解)
lateinit var permissionRequestLauncher: ActivityResultLauncher<String>
接下来,我们在onCreate
方法中对permissionRequestLauncher
进行初始化:使用ComponentActivity.registerForActivityResult
方法返回一个ActivityResultLauncher<I>
对象。
override fun onCreate(savedInstanceState: Bundle?) {
...
permissionRequestLauncher =
registerForActivityResult(RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
doGetPosition()
} else {
doPermisionDenied()
}
}
}
这里让我们来看一下registerForActivityResult
的参数,以便我们理解它的用法。
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return registerForActivityResult(contract, mActivityResultRegistry, callback);
}
对于contract
变量,而上面我们使用的RequestPermission()
生成并返回的的RequestPermission
类对象,就是Activity Result API中定义好的用于请求权限的Contract
类,所以我们只需要直接使用即可。同样的,ActivityResultContract.java
文件中也存在许多其他已经被实现好的(extands)的类,当我们在其他场景下使用Activity Result API时就可以使用这些其他的类。
注意,RequestPermission
类实现的是ActivityResultContract<String, Boolean>
。所以对应的Launch中的Input和Output类型就是String和Boolean类型了。(不记得了?回去看看STEP2吧)
对于callback变量,就比较明显了,这就是一个callback函数。当我们RTFSC的时候,发现它其实只有一个onActivityResult
方法,而它的返回值类型就是刚刚我们提到的Output类型。OK,一切都明了了。所以我们只需要一个返回值为用Boolean表示授权结果的lambda函数就好了。
OK了,现在再回去看看,是不是明白了呢?(如果还不明白,建议再回去看一遍)
注意:
ActivityResultLacuncher<I>
变量的创建时期需要在Activity的onCreate
方法中,或者是Fragment的onAttach
/onCreate
方法中
为什么我会知道呢,给大家看看我当时犯的错x
java.lang.IllegalStateException: Fragment is attempting to registerForActivityResult after being created. Fragments must call registerForActivityResult() before they are created (i.e. initialization, onAttach(), or onCreate()).
STEP4:完善拒绝权限逻辑
在STEP0的流程梳理中提到了,在用户没有选择“拒绝且不再询问”之前,应用仍有机会向用户进行权限用途说明并再次请求权限。那么我们接下来就利用前面所提到的Activity.shouldShowRequestPermission
方法来判断一下是否仍然存在请求权限的机会,并向用户解释权限用途。
fun doPermisionDenied(): Unit {
// determine whether still can request permission & explain
if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)) {
Toast.makeText(this, "permission denied!(still can get permission)", Toast.LENGTH_SHORT)
.show()
// request permission again
permissionRequestLauncher.launch("android.permission.ACCESS_COARSE_LOCATION")
} else
Toast.makeText(this, "permission denied!(go to settings)", Toast.LENGTH_SHORT).show()
}
这里我偷了个懒,没有写引导用户解释权限用途。各位可以根据自己的需求和业务场景选择合适的方案。(才不是想不到什么样比较好呢)
在经过了四个简单的步骤后,我们就已经完成了一个简单的权限获取的功能。相比于RequestPermission,它更简洁明了,更加清晰;同时还甩开了RequestCode这个大包袱,让我们的代码更加清晰。
当然,对于一次性申请多个权限,步骤也是和上面的代码十分相似的,可以参考一下Android Developer Guide
在看完整篇文章后,相信你也对Activity Result API有了更多的好奇。希望掘金上的另一篇文章能解答你更深层的疑问
希望这篇文章能够帮到你。第一次写长文,不足之处还各位请不吝赐教