首頁 > 軟體

Android開發之Kotlin委託的原理與使用詳解

2023-03-23 22:04:22

前言

在設計模式中,委託模式(Delegate Pattern)與代理模式都是我們常用的設計模式(Proxy Pattern),兩者非常的相似,又有細小的區分。

委託模式中,委託物件和被委託物件都是同一型別的物件,委託物件將任務委託給被委託物件來完成。委託模式可以用於實現事件監聽器、回撥函數等功能。

代理模式中,代理物件與被代理物件是兩種不同的物件,代理物件代表被代理物件的功能,代理物件可以控制客戶對被代理物件的存取。代理模式可以用於實現遠端代理、虛擬代理、安全代理等功能。

以類的委託與代理來舉例,委託物件和被委託物件都實現了同一個介面或繼承了同一個類,委託物件將任務委託給被委託物件來完成。代理模式中,代理物件與被代理物件實現了同一個介面或繼承了同一個類,代理物件代表被代理物件,使用者端通過代理物件來存取被代理物件。

兩者的區別:

他們雖然都有同一個介面,主要區別在於委託模式中委託物件和被委託物件是同一型別的物件,而代理模式中代理物件與被代理物件是兩種不同的物件。總的來說,委託模式是為了將方法的實現交給其他類去完成,而代理模式則是為了控制物件的存取,並在存取前後進行額外的操作。

而我們常用的委託模式怎麼使用?在 Java 語言中需要我們手動的實現,而在 Kotlin 語言中直接通過關鍵字 by 就可以實現委託,其實現更加優雅、簡潔了。

我們在開發一個 Android 應用中,常用到的委託分為:

  • 介面/類的委託
  • 屬性的委託
  • 結合lazy的延遲委託
  • 觀察者的委託
  • Map資料的委託

下面我們就一起看看不同種類的委託使用以及在 Android 常見的一些場景中的使用。

一、介面/類委託

我們可以選擇使用介面來實現類似的效果,也可以直接傳參,當然介面的方式更加的靈活,比如我們這裡就以介面比如我定義一個攻擊與防禦的行為介面:

interface IUserAction {

    fun attack()

    fun defense()
}

定義了使用者的行為,有攻擊和防禦兩種操作!接下來我們就定義一個預設的實現類:

class UserActionImpl : IUserAction {

    override fun attack() {
        YYLogUtils.w("預設操作-開始執行攻擊")
    }

    override fun defense() {
        YYLogUtils.w("預設操作-開始執行防禦")
    }
}

都是很簡單的程式碼,我們定義一些預設的操作,如果任意類想擁有攻擊和防禦的能力就直接實現這個介面,如果想自定義攻擊和防禦則重寫對應的方法即可。

如果使用 Java 的方式實現委託,大致程式碼如下:

class UserDelegate1(private val action: IUserAction) : IUserAction {
    override fun attack() {
        YYLogUtils.w("UserDelegate1-需要自己實現攻擊")
    }

    override fun defense() {
        YYLogUtils.w("UserDelegate1-需要自己實現防禦")
    }
}

如果使用 Kotlin 的方式實現則是:

class UserDelegate2(private val action: IUserAction) : IUserAction by action

如果 Kotlin 的實現不想預設的實現也可以重寫部分的操作:

class UserDelegate3(private val action: IUserAction) : IUserAction by action {

    override fun attack() {
        YYLogUtils.w("UserDelegate3 - 只重寫了攻擊")
    }
}

那麼使用起來就是這樣的:

    val actionImpl = UserActionImpl()

    UserDelegate1(actionImpl).run {
        attack()
        defense()
    }

    UserDelegate2(actionImpl).run {
        attack()
        defense()
    }

    UserDelegate3(actionImpl).run {
        attack()
        defense()
    }

列印紀錄檔如下:

其實在 Android 原始碼中也有不少委託的使用,例如生命週期的 Lifecycle 委託:

Lifecycle 通過委託機制實現其功能。具體來說,元件可以將自己的生命週期狀態委託給 LifecycleOwner 物件,LifecycleOwner 物件則負責管理這些元件的生命週期。

例如,在一個 Activity 中,我們可以通過將 Activity 物件作為 LifecycleOwner 物件,並將該物件傳遞給需要註冊生命週期的元件,從而實現元件的生命週期管理。 頁面可以使用 getLifecycle() 方法來獲取它所依賴的 LifecycleOwner 物件的 Lifecycle 範例,並在需要時將自身的生命週期狀態委託給該 Lifecycle 範例。

通過這種委託機制,Lifecycle 實現了一種方便的方式來管理元件的生命週期,避免了手動管理生命週期帶來的麻煩和錯誤。

class AnimUtil private constructor() : DefaultLifecycleObserver {
  
    ...

    private fun addLoopLifecycleObserver() {
        mOwner?.lifecycle?.addObserver(this)
    }

    // 退出頁面的時候釋放資源
    override fun onDestroy(owner: LifecycleOwner) {
        mAnim?.cancel()
        destory()
    }

}

除此之外委託還特別適用於一些可設定的功能,比如 Resutl-Api 的封裝,如果當前頁面需要開啟 startActivityForResult 的功能,就實現這個介面,不需要這個功能就不實現介面,達到可設定的效果。

/**
 * 定義是否需要SAFLauncher
 */
interface ISAFLauncher {

    fun <T : ActivityResultCaller> T.initLauncher()

    fun getLauncher(): GetSAFLauncher?

}

由於程式碼是固定的實現,目標Activity也不需要重新實現,我們只需要實現預設的實現即可:

class SAFLauncher : ISAFLauncher {

    private var safLauncher: GetSAFLauncher? = null

    override fun <T : ActivityResultCaller> T.initLauncher() {
        safLauncher = GetSAFLauncher(this)
    }

    override fun getLauncher(): GetSAFLauncher? = safLauncher

}

使用起來我們直接用預設的實現即可:

class DemoActivity : BaseActivity, ISAFLauncher by SAFLauncher() {

    override fun init() {
        initLauncher()  // 實現了介面還需要初始化Launcher
    }

    fun gotoOtherPage() {
        //使用 Result Launcher 的方式啟動,並獲取到返回值
        getLauncher()?.launch<DemoCircleActivity> { result ->
            val result = result.data?.getStringExtra("text")
            toast("收到返回的資料:$result")
        }

    }

}

這樣是不是就非常簡單了呢?具體如何使用封裝 Result Launcher 可以看看我去年的文章 【傳送門】

二、屬性委託

除了類與介面物件的委託,我們還常用於屬性的委託。

我知道了!這麼弄就行了。

private val textStr by "123"

哎?怎麼報錯了?其實不是這麼用的。

屬性委託和類委託一樣,屬性的委託其實是對屬性的 set/get 方法的委託。

需要我們把 set/get 方法委託給 setValue/getValue 方法,因此被委託類(真實類)需要提供 setValue/getValue 方法,val屬性只需要提供 getValue 方法。

我們修改程式碼如下:

    private val textStr by TextDelegate()

    class TextDelegate {

        operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
            return "我是賦值給與的文字"
        }

    }

列印的結果:

而我們定義一個可讀寫的屬性則可以

  private var textStr by TextDelegate()

    class TextDelegate {

        operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
            return "我是賦值給與的文字"
        }

        operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
            YYLogUtils.w("設定的值為:$value")
        }

    }

    YYLogUtils.w("textStr:$textStr")
    textStr = "abc123"

列印則如下:

為了怕大家寫錯,我們其實可以用介面來限制,唯讀的和讀寫的屬性,我們分別可以用 ReadOnlyProperty 與 ReadWriteProperty 來限制:

    class TextDelegate : ReadOnlyProperty<Any, String> {
        override fun getValue(thisRef: Any, property: KProperty<*>): String {
            return "我是賦值給與的文字"
        }
    }

    class TextDelegate : ReadWriteProperty<Any, String> {
        override fun getValue(thisRef: Any, property: KProperty<*>): String {
            return "我是賦值給與的文字"
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
            YYLogUtils.w("設定的值為:$value")
        }
    }

那麼實現的方式和上面自己實現的效果是一樣的。如果要使用屬性委託可以選用這種介面限制的方式實現。

我們的屬性除了委託給類去實現,同時也能委託給其他屬性(Kotlin 1.4+)來實現,例如:

    private var textStr by TextDelegate2()
    private var textStr2 by this::textStr

其實是內部委託了物件的 get 和 set 函數。相對委託物件而言效能更好一些。而委託物件去實現,不僅增加了一個委託類,而且還還在初始化時就建立了委託類的範例物件,算起來其實效能並不好。

所以屬性的委託不要濫用,如果要用,可以選擇委託現成的其他屬性來完成,或者使用延遲委託Lazy實現,或者使用更簡單的方式實現:

    private val industryName: String
        get() {
            return "abc123"
        }

對於唯讀的屬性,這種方式也是我們常見的使用方式。

三、延遲委託

如果說使用類來實現委託不那麼好的話,其實我們可以使用延遲委託。延遲關鍵字 lazy 接收一個 lambda 表示式,最後一行代表返回值給被推脫的屬性。

預設的 Lazy 實現:

    val name: String by lazy {
        YYLogUtils.w("第一次呼叫初始化")
        "abc123"
    }

    YYLogUtils.w(name)
    YYLogUtils.w(name)
    YYLogUtils.w(name)

只有在第一次使用此屬性的時候才會初始化,一旦初始化之後就可以直接獲取到值。

紀錄檔列印:

它的內部其實也是使用的是類的委託實現。

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

最終的實現是由 SynchronizedLazyImpl 類生成並實現的:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private val lock = lock ?: this

    override val value: T
        get() {
            val _v1 = _value
            if (_v1 !== UNINITIALIZED_VALUE) {
                @Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if (_v2 !== UNINITIALIZED_VALUE) {
                    @Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

我們可以直接看 value 的 get 方法,如果_v1 !== UNINITIALIZED_VALUE 則表明已經初始化過了,就直接返回 value ,否則表明沒有初始化過,呼叫initializer方法,也就是 lazy 的 lambda 表示式返回屬性的賦值。

跟我們自己實現類的委託類似,也是實現了getValue方法。只是多了判斷是否初始化的一些相關邏輯。

lazy的引數分為三種型別:

  • SYNCHRONIZED:新增同步鎖,使lazy延遲初始化執行緒安全
  • PUBLICATION:初始化的lambda表示式,可以在同一時間多次呼叫,但是隻有第一次的返回值作為初始化值
  • NONE:沒有同步鎖,非執行緒安全

預設情況下,對於 lazy 屬性的求值是同步鎖的(synchronized),是可以保證執行緒安全的,但是如果不需要執行緒安全和減少效能花銷可以可以使用 lazy(LazyThreadSafetyMode.NONE){} 即可。

四、觀察者委託

除了對屬性的值進行委託,我們甚至還能對觀察到這個變化過程:

使用 observable 委託監聽值的變化:

    var values: String by Delegates.observable("預設值") { property, oldValue, newValue ->

        YYLogUtils.w("列印值: $oldValue -> $newValue ")
    }

    values = "第一次修改"
    values = "第二次修改"
    values = "第三次修改"

列印:

我們還能使用 vetoable 委託,和 observable 一樣可以觀察屬性的變化,不同的是 vetoable 可以決定是否使用新值。

    var age: Int by Delegates.vetoable(18) { property, oldValue, newValue ->
        newValue > oldValue
    }

    YYLogUtils.w("age:$age")
    age = 14
    YYLogUtils.w("age:$age")
    age = 20
    YYLogUtils.w("age:$age")
    age = 22
    YYLogUtils.w("age:$age")
    age = 20
    YYLogUtils.w("age:$age")

我們需要返回 booble 值覺得是否使用新值,比如上述的例子就是當新值大於老值的時候才賦值。那麼列印的紀錄檔就是如下:

雖然這種方式我們並不常用,一般我們都是使用類似 Flow 之類的工具在源頭就處理了邏輯,使用這種方式我們就可以在屬性的賦值過程中進行攔截了。在一些特定的場景下還是有用的。

五、Map委託

我們的屬性不止可以使用類的委託,延遲的委託,觀察的委託,還能委託Map來進行賦值。

當屬性的值與 Map 中 key 相同的時候,我們可以把對應 key 的 value 取出來並賦值給屬性:

class Member(private val map: Map<String, Any>) {

    val name: String by map
    val age: Int by map
    val dob: Long by map

    override fun toString(): String {
        return "Member(name='$name', age=$age, dob=$dob)"
    }

}

使用:

        val member = Member(mapOf("name" to "guanyu", "age" to 36, Pair("dob", 1234567890L)))
        YYLogUtils.w("member:$member")

列印的紀錄檔:

但是需要注意的是,map 中的 key 名字必須要和屬性的名字一致才行,否則委託後執行解析時會丟擲 NoSuchElementException 異常提示。

例如我們在 Member 物件中加入一個並不存在的 address 屬性,再次執行就會報錯。

而我們把 Int 的 age 屬性賦值給為字串也會報型別轉換異常:

所以一定要一一對應才行哦,我怎麼感覺有一點 TypeScript 結構賦值的那味道 - - !

總結

委託雖好不要濫用。委託畢竟還是中間多了一個委託類,如果沒必要可以直接賦值實現,而不需要多一箇中間類佔用記憶體。

我們可以通過介面委託來實現一些可選的設定。通過委託類實現屬性的監聽與賦值。可以減少一些模板程式碼,達到低耦合高內聚的效果,可以提高程式的可維護性、可延伸性和可重用性。

對於屬性的類委託,我們可以將屬性的讀取和寫入操作委託給另一個物件,或者另一個屬性,或者使用延遲委託來推遲物件的建立直到第一次存取。

對於 map 的委託,我們需要仔細對應屬性與 key 的一致性。以免出現錯誤,這是執行時的錯誤,有可能出現在生產環境上的。

那麼大家都是怎麼使用的呢?有沒有更好的方式呢?或者你有遇到的坑也都可以在評論區交流一下,大家可以互相學習進步。如有本文有一些錯漏的地方,希望同學們可以指出。

到此這篇關於Android開發之Kotlin委託的原理與使用詳解的文章就介紹到這了,更多相關Android Kotlin委託內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com