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

推荐阅读更多精彩内容