> 文章列表 > Android之编写申请权限库PermissionX

Android之编写申请权限库PermissionX

Android之编写申请权限库PermissionX

比如要实现拨打电话的功能,一般我们要编写如下Android运行时权限API

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)if(ContextCompat.checkSelfPermission(this,Manifest.permission.CALL_PHONE)!=PackageManager.PERMISSION_GRANTED){ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CALL_PHONE),1)}else{call()}}override fun onRequestPermissionsResult(requestCode: Int,permissions: Array<out String>,grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)when(requestCode){//如果requestCode是11->{if(grantResults.isNotEmpty()&&grantResults[0]==PackageManager.PERMISSION_GRANTED){call()}else{Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show()}}}}//执行相关打电话操作private fun call() {//}}

可以看到,这种系统内置的运行时权限API的用法还是非常烦琐的,需要先判断用户是否授权我们拨打电话的权限,如果没有的话需要进行权限申请,然后还要在onRequestPermissionsResult()回调中处理权限申请的结果,最后才能去执行拨打电话的操作。
我们可以通过这个过程编写一个开源库PermissionX。
之前我们写的所有代码都是在app目录下进行的。这其实是一个专门用于开发应用程序的模块。而我们现在要开发的是一个库,因此我们需要新建一个模块。
实际上,一个Android项目中可以包含任意多个模块,并且模块与模块之间可以相互引用。比方说,我们在模块A中编写了一个功能,那么只需要在模块B中引入模块A,模块B就可以无缝地使用模块A中提供的所有功能。
在PermissionX项目中新建一个模块,并在这个模块中实现具体的功能。对着最顶层的PermissionX目录右击→New→Module,选择Android Library会弹出如下
Android之编写申请权限库PermissionX
点击“Finish”按钮完成创建,现在PermissionX工程目录下应该就有app和library两个模块了。
Android之编写申请权限库PermissionX
观察一下library模块中的build.gradle文件,其简化后的代码如下所示:

plugins {id 'com.android.library'id 'kotlin-android'
}android {compileSdkVersion 32buildToolsVersion "30.0.3"defaultConfig {minSdkVersion 21targetSdkVersion 32versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"consumerProguardFiles "consumer-rules.pro"}...}...

会发现它和app模块中的build.gradle文件有两个重要的区别:第一,这里头部引入的插件是com.android.library,表示这是一个库模块,而app/build.gradle文件头部引入的插件是com.android.application,表示这是一个应用程序模块;第二,这里的defaultConfig闭包中是不可以配置applicationId属性的,而app/build.gradle中则必须配置这个属性,用于作为应用程序的唯一标识。

想要对运行时权限的API进行封装,这个操作是有特定的上下文依赖的,一般需要在Activity中接收onRequestPermissionsResult()方法的回调才行,所以不能简单地将整个操作封装到一个独立的类中。受此限制以往都是将运行权限的操作封装到BaseActivity中,或者提供一个透明的Activity来处理运行时权限等。
其实Google在Fragment中也提供了一份相同的API,使得我们在Fragment中也能申请运行时权限。不同的是,Fragment并不像Activity那样必须有界面,我们完全可以向Activity中添加一个隐藏的Fragment,然后在这个Fragment中对运行时权限的API进行封装。这是一种轻量级的做法,不用担心隐藏Fragment对Activity性能的影响。

package com.permission.yiranimport android.content.pm.PackageManager
import androidx.fragment.app.Fragmentclass InvisibleFragment: Fragment() {//callback为函数类型变量,可为空private var callback: PermissionCallback? =nullfun requestNow(cb:PermissionCallback,vararg permissions:String){callback=cbrequestPermissions(permissions,1)}override fun onRequestPermissionsResult(requestCode: Int,permissions: Array<out String>,grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)if(requestCode==1){//使用deniedList列表来记录所有被用户拒绝的权限val  deniedList=ArrayList<String>()for((index,result) in grantResults.withIndex()){//如果发现某个权限未被用户授权if(result!=PackageManager.PERMISSION_GRANTED){deniedList.add(permissions[index])}}//标识是否所有申请权限均已被授权,如果为空说明都已授权val allGranted=deniedList.isEmpty()//let函数用于判空,it代表callback对象callback?.let {it(allGranted,deniedList)}}}
}

首先我们定义了一个callback变量作为运行时权限申请结果的回调通知方式,并将它声明成了一种函数类型变量,该函数类型接收Boolean和List< String >这两种类型的参数,并且没有返回值。

然后定义一个requestNow()方法,该方法接收一个与callback变量类型相同的函数类型参数,同时还使用vararg关键字接收了一个可变长度的permissions参数列表。将传递进来的函数类型参数赋值给callback变量,然后调用Fragment中提供的requestPermissions()方法去立即申请运行时权限,并将permissions参数列表传递进去,这样就可以实现由外部调用方自主指定要申请哪些权限的功能了。

接下来还需要重写onRequestPermissionsResult()方法,并在这里处理运行时权限的申请结果。可以看到,我们使用了一个deniedList列表来记录所有被用户拒绝的权限,然后遍历grantResults数组,如果发现某个权限未被用户授权,就将它添加到deniedList中。遍历结束后使用了一个allGranted变量来标识是否所有申请的权限均已被授权,判断的依据就是deniedList列表是否为空。最后使用callback变量对运行时权限的申请结果进行回调。

typealias PermissionCallback=(Boolean, List<String>) -> Unit
class InvisibleFragment: Fragment() {//callback可为空private var callback: PermissionCallback? =nullfun requestNow(cb:PermissionCallback,vararg permissions:String){callback=cbrequestPermissions(permissions,1)}...}

typealias 关键字可以用于给任意类型指定一个别名,比如我们将(Boolean, List< String >) -> Unit的别名指定成了PermissionCallback,这样就可以使用PermissionCallback来替代之前所有使用(Boolean, List< String >) -> Unit的地方。
接下来就是对外接口部分,新建一个PermissionX单例类

package com.permission.yiranimport androidx.fragment.app.FragmentActivityobject PermissionX {private const val TAG="InvisibleFragment"fun request(activity:FragmentActivity,vararg permissions:String,callback:PermissionCallback){val fragmentManager=activity.supportFragmentManagerval existedFragment=fragmentManager.findFragmentByTag(TAG)val fragment=if(existedFragment!=null){existedFragment as InvisibleFragment//大到小强制转换}else{val invisibleFragment=InvisibleFragment()fragmentManager.beginTransaction().add(invisibleFragment,TAG).commitNow()invisibleFragment}fragment.requestNow(callback, *permissions)}
}

将PermissionX指定为单例类,是为了让PermissionX中的接口能够更加方便地被调用。我们在PermissionX中定义了一个request()方法,这个方法接收一个FragmentActivity参数、一个可变长度的permissions参数列表,以及一个callback回调。

在request()方法中,首先获取FragmentManager的实例,然后调用findFragmentByTag()方法来判断传入的Activity参数是否已经包含了指定TAG的Fragment,也就是我们刚才编写的InvisibleFragment。如果已经包含则直接使用该Fragment,否则就创建一个新的InvisibleFragment实例,并将它添加到Activity中,同时指定一个TAG。注意:在添加结束后一定要调用commitNow()方法,而不能调用commit()方法,因为commit()方法并不会立即执行添加操作,因而无法保证下一行代码执行时InvisibleFragment已经被添加到Activity中。

有了InvisibleFragment的实例之后,接下来我们只需要调用它的requestNow()方法就能去申请运行时权限了,申请结果会自动回调到callback参数中。需要注意的是,permissions参数在这里实际上是一个数组。对于数组,我们可以去遍历也可以通过下标访问,但是不可以直接将它传递给另外一个接收可变长度参数的方法。因为,这里在调用requestNow()方法时,在Permissions参数的前面加上一个*,这个符号表示将一个数组转换成可变长度参数传递过去。

对开源库进行测试

我们可以通过在app模块中引入library模块,然后在app模块中使用PermissionX提供的接口编写一些申请运行时权限的代码,看看能否正常工作,以此来验证PermissionX库的正确性。

dependencies {
...implementation project(':Library')
}

接下来编写activity_main.xml文件,在里面加入一个用于拨打电话的按钮

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><Buttonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:text="Make Call"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintLeft_toLeftOf="parent"app:layout_constraintRight_toRightOf="parent"app:layout_constraintTop_toTopOf="parent" /></LinearLayout>

在MainActivity 中申请拨打电话的运行时权限,并实现拨打电话的功能

class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)makeCallBtn.setOnClickListener { PermissionX.request(this,Manifest.permission.CALL_PHONE){allGranted,deniedList->if(allGranted){call()}else{Toast.makeText(this,"You denied $deniedList",Toast.LENGTH_SHORT).show()}}}}private fun call(){try {val intent=Intent(Intent.ACTION_CALL)intent.data= Uri.parse("tel:10086")startActivity(intent)}catch (e:SecurityException){e.printStackTrace()}}
}

只需要调用PermissionX的request()方法,传入当前的Activity和要申请的权限名,然后再Lambda表达式中处理权限的申请结果就可以了。如果allGranted等于true,就说明所有申请的权限都被用户授权了,那么就执行拨打电话的操作,否则使用Toast弹出一条失败的提示。
另外,PermissionX也支持一次性申请多个权限,只需要将所有要申请的权限名都传入request()方法就可以了。
例如:

 PermissionX.request(this,Manifest.permission.CALL_PHONE,Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_CONTACTS){allGranted,deniedList->if(allGranted){Toast.makeText(this,"All permissions are granted",Toast.LENGTH_SHORT).show()}else{Toast.makeText(this,"You denied $deniedList",Toast.LENGTH_SHORT).show()}}

还要记得在AndroidManifest.xml文件中添加拨打电话的权限声明

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

运行效果
点击MAKE CALL按钮
Android之编写申请权限库PermissionX
点击Allow
Android之编写申请权限库PermissionX

Zen Space