Kotlin 委托模式用于 Android 開發(fā)

委托模式被證明是一種很好的替代繼承的方式,Kotlin 在語言層面對委托模式提供了非常優(yōu)雅的支持(語法糖)。

先給大家看看我用 Kotlin 的屬性委托語法糖在 Android 工程里面做的一件有用工作——SharedPreferences 的讀寫委托。

文中陳列的所有代碼已匯總成 Demo 傳至 github,點(diǎn)這兒獲取源碼。建議配合 Demo 閱讀本文。

項(xiàng)目主要文件結(jié)構(gòu)如下:

│  App.kt
│
├─base
│      SpBase.kt
│
├─delegates
│      SPDelegates.kt
│      SPUtils.kt
│
├─demo
└─ui
        MainActivity.kt

先來看看 delegates 包下的文件。

SPUtils 是個讀寫 SharedPreferences(以下簡稱 SP) 存儲項(xiàng)的基礎(chǔ)工具類:

/**
 * @author xiaofei_dev
 * @desc 讀寫 SP 存儲項(xiàng)的基礎(chǔ)工具類
 */

object SPUtils {
    val SP by lazy {
        App.instance.getSharedPreferences("default", Context.MODE_PRIVATE)
    }

    //讀 SP 存儲項(xiàng)
    fun <T> getValue(name: String, default: T): T = with(SP) {
        val res: Any = when (default) {
            is Long -> getLong(name, default)
            is String -> getString(name, default) ?: ""
            is Int -> getInt(name, default)
            is Boolean -> getBoolean(name, default)
            is Float -> getFloat(name, default)
            else -> throw java.lang.IllegalArgumentException()
        }
        @Suppress("UNCHECKED_CAST")
        res as T
    }

    //寫 SP 存儲項(xiàng)
    fun <T> putValue(name: String, value: T) = with(SP.edit()) {
        when (value) {
            is Long -> putLong(name, value)
            is String -> putString(name, value)
            is Int -> putInt(name, value)
            is Boolean -> putBoolean(name, value)
            is Float -> putFloat(name, value)
            else -> throw IllegalArgumentException("This type can't be saved into Preferences")
        }.apply()
    }
}

主要使用泛型實(shí)現(xiàn)了完善的 SP 讀寫,整體還是非常簡潔易懂的。上下文對象使用了自定義的 Application 類實(shí)例(見 Demo 中的 App 類)。

Kotlin 中的委托屬性

下面重點(diǎn)來看一下 SPDelegates 類的定義:

/**
 * @author xiaofei_dev
 * @desc <p>讀寫 SP 存儲項(xiàng)的輕量級委托類,如下,
 * 讀 SP 的操作委托給該類對象的 getValue 方法,
 * 寫 SP 操作委托給該類對象的 setValue 方法,
 * 注意這兩個方法不用你顯式調(diào)用,把一切交給編譯器就行(還是語法糖)
 * 具體使用此類定義 SP 存儲項(xiàng)的代碼請參考 SpBase 文件</p>
 */

class SPDelegates<T>(private val key: String, private val default: T) : ReadWriteProperty<Any?, T> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return SPUtils.getValue(key, default)
    }
    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        SPUtils.putValue(key, value)
    }
}

SPDelegates 類實(shí)現(xiàn)了 Kotlin 標(biāo)準(zhǔn)庫中聲明的用于屬性委托的 ReadWriteProperty 接口(SPDelegates 類的用法后面會詳細(xì)說到),從名字可以看出此接口是可讀寫的(適用于 var 聲明的屬性),除此之外還有個 ReadOnlyProperty (只讀)接口(適用于 val 聲明的屬性)。

對于屬性的委托類(以SPDelegates為例),要求必須提供一個 getValue() 函數(shù)(和一個setValue()函數(shù)——對于 var 屬性)。其getValue 方法的參數(shù)要求如下:

  • thisRef —— 必須與 屬性所屬類 的類型(對于擴(kuò)展屬性——指被擴(kuò)展的類型)相同或是它的超類型(參見后面 SpBase 單例類中的注釋);
  • property —— 必須是類型 KProperty<*> (Kotlin 標(biāo)準(zhǔn)庫 kotlin.reflect (反射)包下的一個類)或其超類型。

對于其 setValue 方法,前兩個參數(shù)同 getValue。第三個參數(shù)value 必須與屬性同類型或是它的子類型。

以上概念暫時看不懂不要緊,下面通過委托屬性的具體應(yīng)用來加深理解。

接著是具體使用到委托屬性的 SpBase 單例類:

/**
 * @author xiaofei_dev
 * @desc 定義的 SP 存儲項(xiàng)
 */
object SpBase{
    //SP 存儲項(xiàng)的鍵
    private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"


    // 這就定義了一個 SP 存儲項(xiàng)
    // 把 SP 的讀寫操作委托給 SPDelegates 類的一個實(shí)例(使用 by 關(guān)鍵字,by 是 Kotlin 語言層面的一個原語),
    // 此時訪問 SpBase 的 contentSomething (你可以簡單把其看成 Java 里的一個靜態(tài)變量)屬性即是在讀取 SP 的存儲項(xiàng),
    // 給 contentSomething 屬性賦值即是寫 SP 的操作,就這么簡單
    // 這里用到的 SPDelegates 對象的 getValue 方法的 thisRef(見上文) 參數(shù)的類型正是外層的 SpBase
    var contentSomething: String by SPDelegates(CONTENT_SOMETHING, "我是一個 SP 存儲項(xiàng),點(diǎn)擊編輯我")
}

上面代碼中,單例 SpBase 的屬性 contentSomething 就是一個定義好的 SP 存儲項(xiàng)。得益于語言級別的強(qiáng)大語法糖支持,寫出來的代碼可以如此簡潔而優(yōu)雅。讀寫 SP 存儲項(xiàng)的請求通過屬性委托給了一個 SPDelegates 對象,委托屬性的語法為

val/var <屬性名>: <類型> by <表達(dá)式>

其最后會被編譯器解釋成下面這樣的代碼(大致上):

object SpBase{
    private const val CONTENT_SOMETHING = "CONTENT_SOMETHING"
    
    private val propDelegate = SPDelegates(CONTENT_SOMETHING, "我是一個 SP 存儲項(xiàng),點(diǎn)擊編輯我")
    var contentSomething: String
        get() = propDelegate.getValue(this, this::contentSomething)//讀SP
        set(value) = propDelegate.setValue(this, this::contentSomething, value)//寫SP
}

還是比較容易理解的。下面演示下這個定義好的 SP 存儲項(xiàng)如何使用,見 Demo 的 MainActivity 類文件:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initView()
    }

    private fun initView(){
        //讀取 SP 內(nèi)容顯示到界面上
        editContent.setText(SpBase.contentSomething)
        btnSave.setOnClickListener {
            //保存 SP 項(xiàng)
            SpBase.contentSomething = "${editContent.text}"
            Toast.makeText(this, R.string.main_save_success, Toast.LENGTH_SHORT).show()
        }
    }
}

整體比較簡單,就是個讀寫 SP 存儲項(xiàng)的過程。大家可以實(shí)際運(yùn)行下 Demo 看下具體效果。

從零實(shí)現(xiàn)一個屬性的委托類

上文述及的 SPDelegates 類實(shí)現(xiàn)了 Kotlin 標(biāo)準(zhǔn)庫提供的 ReadWriteProperty 接口,我們當(dāng)然也可以不借助任何接口來實(shí)現(xiàn)一個屬性委托類,只要其提供一個getValue() 函數(shù)(和一個setValue()函數(shù)——對于 var 屬性)并且符合我們上面討論的參數(shù)要求就行。下面來定義一個平凡的屬性委托類 Delegate (見 Demo 的 demo 包下 Example 文件):

/**
 * @author xiaofei_dev
 * @desc 不用實(shí)現(xiàn)任何接口的平凡屬性委托類
 */

class Delegate<T> {
    private var value: T? = null

    operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
        println("$thisRef, thank you for delegating '${property.name}' to me! The value is $value")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
        this.value = value
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

使用方式依舊:

class Example {
    //委托屬性
    var p: String? by Delegate()
}

fun main(args: Array<String>) {
    val e = Example()
    e.p = "hehe"
    println(e.p)
}

控制臺輸出如下:

hehe has been assigned to 'p' in com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb.
com.xiaofeidev.delegatedemo.demo.Example@1fb3ebeb, thank you for delegating 'p' to me! The value is hehe
hehe

你可以自己跑下試試~

關(guān)于委托模式

有必要單獨(dú)花篇幅解釋下何為委托模式。

簡而言之,在委托模式中,有兩個對象共同處理同一個請求,接受請求的對象將請求委托給另一個對象來處理。

委托模式最簡單的例子:

//委托類,墨水能用來打印文字( ̄▽ ̄)"
class Ink {
    fun print() {
        print("This message comes from the delegate class,Not Printer.")
    }
}

class Printer {
    //委托對象
    var ink = Ink()

    fun print() {
        //Printer 的實(shí)例會將請求委托給另一個對象(DelegateNormal 的對象)來處理
        ink.print()//調(diào)用委托對象的方法
    }
}

fun main(args: Array<String>) {
    val printer = Printer()
    printer.print()
}

控制臺輸出如下:

This message comes from the delegate class,Not Printer.

委托模式使我們可以用聚合來代替繼承,是許多其他設(shè)計(jì)模式(如狀態(tài)模式、策略模式、訪問者模式)的基礎(chǔ)。

Kotlin 的委托模式

Kotlin 可以做到零樣板代碼實(shí)現(xiàn)委托模式(而不是像上面展示的那樣還需要樣板代碼)!

比如我們現(xiàn)在有如下接口和類:

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

Base 接口想做的就是在控制臺打印些什么東西。這沒啥問題,我們已經(jīng)在 BaseImpl 類上完整實(shí)現(xiàn)了 Base 接口。

此時我們想再給 Base 接口寫一個實(shí)現(xiàn)時可以這么做:

class Derived(b: Base) : Base by b

這其實(shí)跟下面的寫法是等價的(編譯器實(shí)際生成的):

class Derived(val delegate: Base) : Base {
    override fun print() {
        delegate.print()
    }
}

注意不是下面這種:

class Derived(val delegate: Base){
    fun print() {
        delegate.print()
    }
}

Kotlin 通過編譯器的黑魔法將許多樣板代碼封印在了 by 這樣一個語言級別的原語中(又是語法糖)。使用方式:

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print()
}

控制臺輸出如下:

10

Kotlin 標(biāo)準(zhǔn)庫中其他屬性委托

說回屬性委托,Kotlin 的標(biāo)準(zhǔn)庫為一些常用的委托寫好了工廠方法,下面一一列舉。

延遲屬性 Lazy

fun main(args: Array<String>) {
    //延遲計(jì)算屬性的值,lazy 后面 lambda 表達(dá)式中的邏輯只會執(zhí)行一次(且是線程安全的)并記錄結(jié)果,后續(xù)調(diào)用屬性的 get() 方法只是返回記錄的結(jié)果
    val lazyValue: String by lazy {
        println("computed!")
        "Hello"
    }
    println(lazyValue)
    println(lazyValue)
}

控制臺輸出如下:

computed!
Hello
Hello

可觀察屬性 Observable

Delegates.observable()接受兩個參數(shù):初始值與修改時處理程序。 每次給屬性賦值時就會調(diào)用該處理程序(在賦值執(zhí)行)。處理程序有三個參數(shù):被賦值屬性的 KProperty 對象、舊值與新值:

class User {
    var name: String by Delegates.observable("<no name>") {
            prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

控制臺輸出如下:

<no name> -> first
first -> second

把屬性儲存在映射中

你甚至可以在一個映射(map)中存儲屬性的值。 這種情況下,你可以直接將屬性委托給映射實(shí)例:

class Student(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

fun main(args: Array<String>) {
    val student = Student(mapOf(
        "name" to "xiaofei",
        "age"  to 25
    ))

    println(student.name)
    println(student.age)
}

當(dāng)然這種應(yīng)用必須確保屬性的名字和 map中的鍵對應(yīng)起來,不然你可能會收獲一個 NoSuchElementException 運(yùn)行時異常,大概像這樣:

java.util.NoSuchElementException: Key XXXX is missing in the map.

言止于此,未完待續(xù)。

參考文獻(xiàn)

  1. 《設(shè)計(jì)模式:可復(fù)用面向?qū)ο筌浖幕A(chǔ)》
  2. 《Kotlin for Android Developers》
  3. 《Kotlin 極簡教程》
  4. 《Kotlin 核心編程》
  5. 維基百科 委托模式 詞條
  6. Kotlin 官方文檔
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容