Kotlin 委託模式用於 Android 開發

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

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

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

項目主要文件結構如下:

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

先來看看 delegates 包下的文件。

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

/**
 * @author xiaofei_dev
 * @desc 讀寫 SP 存儲項的基礎工具類
 */

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

    //讀 SP 存儲項
    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 存儲項
    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()
    }
}

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

Kotlin 中的委託屬性

下面重點來看一下 SPDelegates 類的定義:

/**
 * @author xiaofei_dev
 * @desc <p>讀寫 SP 存儲項的輕量級委託類,如下,
 * 讀 SP 的操作委託給該類對象的 getValue 方法,
 * 寫 SP 操作委託給該類對象的 setValue 方法,
 * 注意這兩個方法不用你顯式調用,把一切交給編譯器就行(還是語法糖)
 * 具體使用此類定義 SP 存儲項的代碼請參考 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 類實現了 Kotlin 標準庫中聲明的用於屬性委託的 ReadWriteProperty 接口(SPDelegates 類的用法後面會詳細說到),從名字可以看出此接口是可讀寫的(適用於 var 聲明的屬性),除此之外還有個 ReadOnlyProperty (只讀)接口(適用於 val 聲明的屬性)。

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

  • thisRef —— 必須與 屬性所屬類 的類型(對於擴展屬性——指被擴展的類型)相同或是它的超類型(參見後面 SpBase 單例類中的註釋);
  • property —— 必須是類型 KProperty<*> (Kotlin 標準庫 kotlin.reflect (反射)包下的一個類)或其超類型。

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

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

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

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


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

上面代碼中,單例 SpBase 的屬性 contentSomething 就是一個定義好的 SP 存儲項。得益於語言級別的強大語法糖支持,寫出來的代碼可以如此簡潔而優雅。讀寫 SP 存儲項的請求通過屬性委託給了一個 SPDelegates 對象,委託屬性的語法爲

val/var <屬性名>: <類型> by <表達式>

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

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

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

class MainActivity : AppCompatActivity() {

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

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

整體比較簡單,就是個讀寫 SP 存儲項的過程。大家可以實際運行下 Demo 看下具體效果。

從零實現一個屬性的委託類

上文述及的 SPDelegates 類實現了 Kotlin 標準庫提供的 ReadWriteProperty 接口,我們當然也可以不借助任何接口來實現一個屬性委託類,只要其提供一個getValue() 函數(和一個setValue()函數——對於 var 屬性)並且符合我們上面討論的參數要求就行。下面來定義一個平凡的屬性委託類 Delegate (見 Demo 的 demo 包下 Example 文件):

/**
 * @author xiaofei_dev
 * @desc 不用實現任何接口的平凡屬性委託類
 */

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

你可以自己跑下試試~

關於委託模式

有必要單獨花篇幅解釋下何爲委託模式

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

委託模式最簡單的例子:

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

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

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

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

控制檯輸出如下:

This message comes from the delegate class,Not Printer.

委託模式使我們可以用聚合來代替繼承,是許多其他設計模式(如狀態模式、策略模式、訪問者模式)的基礎。

Kotlin 的委託模式

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

比如我們現在有如下接口和類:

interface Base {
    fun print()
}

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

Base 接口想做的就是在控制檯打印些什麼東西。這沒啥問題,我們已經在 BaseImpl 類上完整實現了 Base 接口。

此時我們想再給 Base 接口寫一個實現時可以這麼做:

class Derived(b: Base) : Base by b

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

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 標準庫中其他屬性委託

說回屬性委託,Kotlin 的標準庫爲一些常用的委託寫好了工廠方法,下面一一列舉。

延遲屬性 Lazy

fun main(args: Array<String>) {
    //延遲計算屬性的值,lazy 後面 lambda 表達式中的邏輯只會執行一次(且是線程安全的)並記錄結果,後續調用屬性的 get() 方法只是返回記錄的結果
    val lazyValue: String by lazy {
        println("computed!")
        "Hello"
    }
    println(lazyValue)
    println(lazyValue)
}

控制檯輸出如下:

computed!
Hello
Hello

可觀察屬性 Observable

Delegates.observable()接受兩個參數:初始值與修改時處理程序。 每次給屬性賦值時就會調用該處理程序(在賦值執行)。處理程序有三個參數:被賦值屬性的 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)中存儲屬性的值。 這種情況下,你可以直接將屬性委託給映射實例:

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)
}

當然這種應用必須確保屬性的名字和 map中的鍵對應起來,不然你可能會收穫一個 NoSuchElementException 運行時異常,大概像這樣:

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

言止於此,未完待續。

參考文獻

  1. 《設計模式:可複用面向對象軟件的基礎》
  2. 《Kotlin for Android Developers》
  3. 《Kotlin 極簡教程》
  4. 《Kotlin 核心編程》
  5. 維基百科 委託模式 詞條
  6. Kotlin 官方文檔
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章