android 許可權元件設計

血色王冠發表於2019-05-06

ps:打算從簡書搬到掘金來了,簡書沒討論氛圍,發了好久也不見有人討論,還是掘金人氣旺,氛圍好,記得16年剛註冊那會還不讓發帖呢,申請作家都不給通過...

美女鎮樓

有輪子就不要再自己造輪子了,這是行業公認的,我這裡不是從頭寫一個許可權庫,而是在開源元件上再封裝以統一公司內部呼叫,隨時可以替換第三方實現,從開源庫再封裝這個角度來寫文章的很少,我這裡帶大家領略另一番風景,先說好不喜勿噴啊,我這水平幼兒園都沒畢業呢

專案地址: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 裡面可以找到這個類

相關文章