android 组件设计 - 权限组件封装

美女镇楼


有轮子就不要再自己造轮子了,这是行业公认的,我这里不是从头写一个权限库,而是在开源组件上再封装以统一公司内部调用,随时可以替换第三方实现,从开源库再封装这个角度来写文章的很少,我这里带大家领略另一番风景,先说好不喜勿喷啊,我这水平和大家一样也是幼儿园水准

项目地址:BW_Libs


先看下 Demo 的 代码

不上 gif 了,录这个时间太长,gif 太大网页很卡。Demo 的思路如下,正常的判断权限,有3个回调,用户确认给予权限,用户不给,和用户点选不在显示系统权限弹窗。这里我们在用户不显示弹窗后的回调里启动系统权限设置页,在用户关闭权限设置页面过后,我们再检测下=刚刚用户给没给权限,没给权限的话就自己显示个弹窗,提示用户不给权限就关闭页面

Demo 代码如下:

class PermissionActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_permission)

        btn_permission.setOnClickListener({

            PermissionManage
                    .with(this)
                    .permission(Manifest.permission.CALL_PHONE)
                    .permission(Manifest.permission.CAMERA)
                    .permission(Manifest.permission.READ_PHONE_STATE)
                    .onSuccess { Toast.makeText(this@PermissionActivity, "申请成功", Toast.LENGTH_SHORT).show() }
                    .onDenial { Toast.makeText(this@PermissionActivity, "用户拒绝", Toast.LENGTH_SHORT).show() }
                    .onDontShow { IntentUtils.startSettingActivityForResult(this, 200) }
                    .run()
        })
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

        if (requestCode == 200) {

            var permissions = listOf(Manifest.permission.CALL_PHONE, Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE)
            if (PermissionManage.isHavePermissions(this, permissions)) {
                Toast.makeText(this, "欢迎您给予的权限", Toast.LENGTH_SHORT).show()
            } else {
                showDialog()
            }
        }
    }

    private fun showDialog() {

        var build: AlertDialog.Builder = AlertDialog.Builder(this)
        build.setMessage("缺乏权限,请求您给予权限")
        build.setPositiveButton("申请权限", object : DialogInterface.OnClickListener {
            override fun onClick(dialog: DialogInterface?, which: Int) {
                IntentUtils.startSettingActivityForResult(this@PermissionActivity, 200)
                dialog?.dismiss()
//                dialog?.cancel()
            }
        })
        build.setNegativeButton("不给权限", object : DialogInterface.OnClickListener {
            override fun onClick(dialog: DialogInterface?, which: Int) {
                Toast.makeText(this@PermissionActivity, "对不起,某些权限是必备选择,不给权限不能运行", Toast.LENGTH_SHORT).show()
                dialog?.dismiss()
                this@PermissionActivity.finish()
            }
        })
        build.show()
    }

}

组件封装思路

1. 要好看,相应函数式编程思想,提供链式调用

这点很重要,组件封装完了是要给小伙伴们和自己用的,用起来费时费力,不好理解,不清不楚的都不行,达不到简单易懂易用的组件都是不合格的,从这点来说,其实 Fresco 的 API 就不是很好,当然 Fresco 是非复杂就是了,但是另一个图片加载的小伙伴 Glide API 就很 Nice 啦

这部分就是我封装的权限组件的 API 了,仿照函数式编程,提供链式调用,这样的代码很好看,非常容易理解逻辑。函数式编程在处理连续复杂逻辑的代码上有天然的优势,其风格以清晰著称,是我们封装工具类组件的不二选择

            PermissionManage
                    .with(this)
                    .permission(Manifest.permission.CALL_PHONE)
                    .permission(Manifest.permission.CAMERA)
                    .permission(Manifest.permission.READ_PHONE_STATE)
                    .onSuccess { Toast.makeText(this@PermissionActivity, "申请成功", Toast.LENGTH_SHORT).show() }
                    .onDenial { Toast.makeText(this@PermissionActivity, "用户拒绝", Toast.LENGTH_SHORT).show() }
                    .onDontShow { IntentUtils.startSettingActivityForResult(this, 200) }
                    .run()
2. 第三方库高度,无痕可替换

我使用的是 AndPermission 这个开源库,来看下我对 AndPermission 的包装

2.1 - 抽取接口
第三方权限库是干嘛的,就是提供权限申请验证的,抽泣其公共功能,就是1个,给参数然后执行,就是这么简单,为什么,因为功能单一呗,即使请求验证权限

interface IPermissionExecuter {

    fun run(permissionConfig: PermissionConfig)
}

2.2 - 实现接口 ,包装第三方库

class AndPermissinExecuterImpl : IPermissionExecuter {

    override fun run(permissionConfig: PermissionConfig) {
        AndPermission.with(permissionConfig.context)
                .permission(permissionConfig.permissions.toTypedArray())
                // 用户给权限了
                .onGranted({ permissions: List<String> -> permissionConfig.onSuccessAction() })
                // 用户拒绝权限,包括不再显示权限弹窗也在此列
                .onDenied({ permissions: List<String> ->
                    // 判断用户是不是不再显示权限弹窗了,若不再显示的话进入权限设置页
                    if (AndPermission.hasAlwaysDeniedPermission(permissionConfig.context, permissions)) {
                        // 打开权限设置页
                        permissionConfig.onDontShowAction()
                        return@onDenied
                    }
                    permissionConfig.onDenialAction()
                })
                .start()
    }
}

第三方权限库需要的参数基本都一样,不管有什么,我们都包装到 PermissionConfig 里面,然后通过PermissionConfig 把参数传进来执行,这样我们想要替换 第三方实现时,只要再写一个 IPermissionExecuter 的实现类就行了

2.3 - 提供一个工厂类,实现切换管理

这个就不用说了吧,工厂模式,打击都熟悉的套路了

object ExecuterFactor {

    @JvmField
    val AND_PERMISSION = "AND_PERMISSION"

    @JvmField
    val RX_PERMISSION = "RX_PERMISSION"

    @JvmField
    val DEFAULT_EXECUTER = AND_PERMISSION

    @JvmStatic
    fun getInstance(): IPermissionExecuter {
        return getInstance(DEFAULT_EXECUTER)
    }

    @JvmStatic
    private fun getInstance(type: String): IPermissionExecuter {

        return when (type) {
            AND_PERMISSION -> AndPermissinExecuterImpl()
            DEFAULT_EXECUTER -> AndPermissinExecuterImpl()
            else -> AndPermissinExecuterImpl()
        }
    }
}
3. 抽取公共参数,使用 build 构建

上面我们把第三方所需参数包装成了一个类 PermissionConfig,我们来看看这个类

class PermissionConfig {

    lateinit var context: Context
    // 权限申请成功时回调
    var onSuccessAction: () -> Unit = {}
    // 权限申请失败时回调
    var onDenialAction: () -> Unit = {}
    // 用户设置不显示权限申请弹窗时回调
    var onDontShowAction: () -> Unit = {}
    // 权限集合
    var permissions = mutableListOf<String>()

    private var type: String = ExecuterFactor.DEFAULT_EXECUTER

    /**
     * 添加权限
     */
    fun addPermission(permission: String) {
        if (!permission.isEmpty()) permissions.add(permission)
    }

    /**
     * 设置类型
     */
    fun setType(type: String): PermissionConfig {
        if (!type.isEmpty()) this.type = type
        return this
    }

    /**
     * 执行操作
     */
    fun run() {
        ExecuterFactor.getInstance().run(this)
    }
}

抽取出来的参数没几个,很好理解,所需要的参数,用集合来接收因为可能有多个嘛,然后是3个回调,同意,不同意,关闭权限弹窗,借助 kotlin 的语言,我们不要再像 java 一样去写一个接口了,直接声明成空实现就行,也没有 null 的问题,然后提供设置这几个参数的方法即可

但是吧,我们不能就这么直接使用 PermissionConfig ,因为以后随着时间推移有变化,我们需要一个统一的地方来统一构建参数包装对象,这就是大家熟悉的 build 模式啦

class PermissionBuild(var context: Context) {

    var permissionConfig: PermissionConfig = PermissionConfig()

    init {
        permissionConfig.context = this.context
    }

    /**
     * 设置类型
     */
    fun type(type: String): PermissionBuild {
        if (!type.isEmpty()) permissionConfig.setType(type)
        return this
    }

    /**
     * 添加权限
     */
    fun permission(permission: String): PermissionBuild {
        if (!permission.isEmpty()) permissionConfig?.addPermission(permission)
        return this
    }

    /**
     * 添加成功操作
     */
    fun onSuccess(onSuccessAction: () -> Unit): PermissionBuild {
        if (onSuccessAction != null) permissionConfig.onSuccessAction = onSuccessAction
        return this
    }

    /**
     * 添加失败操作
     */
    fun onDenial(onDenialAction: () -> Unit): PermissionBuild {
        if (onDenialAction != null) permissionConfig.onDenialAction = onDenialAction
        return this
    }

    /**
     * 添加不显示权限弹窗操作
     */
    fun onDontShow(onDontShowAction: () -> Unit): PermissionBuild {
        if (onDontShowAction != null) permissionConfig.onDontShowAction = onDontShowAction
        return this
    }

    /**
     * 执行操作
     */
    fun run() {
        permissionConfig.run()
    }
}

这里的 build 逻辑很简单,不写也可以,本着练手的原则还是写了,还是得益于 kotlin 的语法,方法可以直接接收函数参数,也不用我们再去写接口了,的确是方便了很多,尤其是在我们写的时候可以一气呵成,不用来回切换类,不会打断思路是非常好的

4. 提供统一入口

作为工具类,要有一个统一的入口,静态的也行,new 对象也行,这里推荐使用静态方法的方式,方便理解

class PermissionManage {

    /**
     * 提供相关静态入口,效仿 Glide 通过 with 绑定上下文
     *
     */
    companion object {

        @JvmStatic
        fun with(context: Context): PermissionBuild {
            return PermissionBuild(context)
        }

        @JvmStatic
        fun isHavePermission(context: Context, permission: String): Boolean {
            return PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, permission)
        }

        @JvmStatic
        fun isHavePermissions(context: Context, permissions: List<String>): Boolean {

            for (it in permissions) {
                if (!(PackageManager.PERMISSION_GRANTED == ContextCompat.checkSelfPermission(context, it))) return false
            }
            return true
        }
    }
}

类似于 Glide.with 在添加上下文之后返回 PermissionBuild 构建器用于添加数据,另外还提供了其他工具方法,用来查询是否有权限

我是从后向前走的顺序,思路是基于一步步的实现的,中间的类都是基于我要包装第三方库的目的一步步产生的,简单的功能库基本都是基于这个角度来做


看下类结构

这个组件很简单的,其实我之前用 java 写过这个权限组件,同样的思路,地址在这里:简单对权限开源库进行功能性封装 ,一开始我就是想把 java 换成 kotlin ,但是改改写写,最后基本彻底抛弃以前的重新整理思路写了一遍,旧的那个组件现在看来废话太多,我差不多删了一半的类,另外我又重新考虑了一遍命名,也是基本改了一半的,这个命名在我来看是最难的

新版类结构
老版类结构

这么一看是不是很简单啊,虽然简单但是很好的完成了我们的目标,包装第三方组件,提供统一 API 实现,动态无缝切换第三放实现

数数我们用了几个套路:

  • 提取相同,抽象不同 - 模板模式
    包装第三方实现,抽取 run 执行这个动作,把所以参数包装成统一的配置类

  • 统一切换不同第三方实现 - 工厂模式

  • build 统一构建数据 - 构造者模式

  • 提供统一接口 - 门板模式

上面基本都是套路,权限这个组件功能单一,大家可能体会不到上面这几个套路的神奇,这东西只能靠意淫来理解深化,得自己手把手的写才能有切身体会,才能最终荣辉贯通, UML 类图有时间再放上吧


最后吐槽下

在写这个组件时,测试时尼玛我居然忘了在配置文件里声明所需权限了,我怎么调试怎么都不对,我查了好多遍代码也没找到问题,老打击人了,老郁闷了,让我一天的心情都老差劲了,浪费了2个多小时时间,后来想起来了,尼玛我是抽了自己5分钟的嘴巴子,太丢了

奉劝大家遇到问题时一定要冷静啊,不冷静的后果就是浪费人生,明明很简单的事,代码也写的很好,一次过,就是忘了配置文件这件事,其他都没问题,但是就偏偏好事变坏事,哎,冷静,遇事千万要平常心,要不自己真遭罪

还有就是碎片化这个问题了,小米,魅族,华为手机用公版代码是打不开权限设置页面的,非的要适配,真他妈蛋疼,我又取百度了下,还好直接就找找了,写了工具类,但是考虑了下,这个工具是打开系统页面的,我就没放在权限组件里,从工能上讲风马牛不相及的事不能放一起的,这个类叫:IntentUtils ,具体我就不方了 Demo 里面可以找到这个类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345