饭窝

活捉一只 Android 狗

Loshine's avatar Loshine

Kotlin中的委托属性

Kotlin 是 Jetbrain 推出的一门运行在 JVM 上的语言,它结合了面向对象以及函数式语言的特性,超甜的语法糖以及来自知名 IDE 大厂 Jetbrain 的出身让它初一面世就广受瞩目,特别是在 Android 开发社区中。它相比起 Java 拥有了许许多多的优秀特性,并且几乎每一个新特性都对应解决了 Java 开发时的痛苦之处,本篇文章主要讲解 Kotlin 中的委托属性这一特性。

委托属性(Delegated Properties)

我们先看看官网的定义:

有一些种类的属性,虽然我们可以在每次需要的时候手动实现它们,但是如果能够把他们之实现一次 并放入一个库同时又能够一直使用它们那会更好。例如:

  • 延迟属性(lazy properties): 数值只在第一次被访问的时候计算。
  • 可控性(observable properties): 监听器得到关于这个特性变化的通知,
  • 把所有特性储存在一个映射结构中,而不是分开每一条。

为了支持这些(或者其他)例子,Kotlin 采用 委托属性。

简言之就是简化手动实现的属性,将其抽象出一个库

如何使用

定义一个委托

Kotlin 中有两种属性:用var修饰的可变属性和由val修饰的只读属性。由val修饰的只读属性使用的委托需要实现ReadOnlyProperty,而var修饰的可变属性则需要实现ReadWriteProperty

在调用被委托的属性的gettersetter时,对应操作会被委托给getValue()以及setValue()

如实现一个最简单的委托Delegate

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

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

使用定义好的委托属性

语法为val/var <property name>: <Type> by <expression>

class Example {
    var p: String by Delegate()
}

by后面的是委托表达式,我们调用这个对象并使用属性:

val e = Example()
println(e.p)

e.p = "NEW"

打印结果为:

Example@33a17727, thank you for delegating 'p' to me!
NEW has been assigned to 'p' in Example@33a17727.

如上可知,thisRef对应的是拥有该被委托属性的对象实例,property则是属性,value是调用setter时的传入值。

实例讲解

lazy 懒加载

Kotlin 标准库自带的懒加载委托,在属性第一次被使用时才进行初始化。

函数lazy()接受一个 lambda 然后返回一个可以作为委托Lazy<T> 实例来实现延迟属性: 第一个调用getter执行变量传递到lazy()并记录结果, 后来的getter调用只会返回记录的结果。

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

其打印结果:

computed!   # 第一次使用时先初始化
Hello       # getter
Hello       # 后续都只会调用 getter

懒加载委托在实际编码中应用十分广泛,比如 Android 中我们可以把很多在OnCreate中需要进行的初始化操作使用懒加载委托来实现。

使用委托操作 SharedPreferences

本例出自《Kotlin for Android Developer》,使用了when表达式和委托属性巧妙地使得SharedPrefences的读写变得十分简便

class Preference<T>(val context: Context, val name: String, val default: T) : ReadWriteProperty<Any?, T> {

    val prefs by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) }

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        return findPreference(name, default)
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        putPreference(name, value)
    }

    private fun <U> findPreference(name: String, default: U): U = with(prefs) {
        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 IllegalArgumentException("This type can be saved into Preferences")
        }

        res as U
    }

    private fun <U> putPreference(name: String, value: U) = with(prefs.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 be saved into Preferences")
        }.apply()
    }
}

在代码中我们可以如下使用

class WhateverActivity : Activity() {
    var aInt: Int by Preference(this, "aInt", 0)

    fun whatever() {
        println(aInt) // 会从 SharedPreference 取这个数据
        aInt = 9 // 会将这个数据写入 SharedPreference
    }
}

从此操作SharedPreferences变得如此简单 ~

简单实现一个 KotterKnife

KotterKnife 是一个 Android 控件依赖注入框架,使用它可以很方便地初始化 Activity、Fragment、View 等的控件。

KotterKnife 的实现原理就是使用了委托属性,下面我就使用委托属性简单实现一个 View 注入功能

实现

我们平时是这样初始化 View 的

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

    val textView = findViewById(R.id.text_view) as TextView
}

考虑到通常我们在onCreate方法中将其初始化,我们可以用 lazy 委托,在第一次使用该控件的时候才将其初始化,这样可以减少不必要的内存消耗。

val mTextView by lazy {
    findViewById(R.id.text_view) as TextView
}

对其抽取简化

@Suppress("UNCHECKED_CAST")
fun <V : View> Activity.bindView(id: Int): Lazy<V> = lazy {
    viewFinder(id) as V
}

private val Activity.viewFinder: Activity.(Int) -> View?
    get() = { findViewById(it) }

之后我们就可以在 Activity 中这样注入 View 了

val mTextView by bindView<TextView>(R.id.text_view)

如上实现了类似 KotterKnife 的控件注入功能,当然 KotterKnife 中还有更加强大的可选绑定以及数组绑定,本文中我们就不细说了,有兴趣的读者可以阅读 KotterKnife源码

小结

本文分析了 Kotlin 中的委托属性,并对其实际应用做了示例分析。委托属性是 Kotlin 语言的一个特性,灵活使用可以解决实际编码中的许多问题,减少大量重复代码,而由于其与属性的gettersetter直接绑定所以使用起来也十分灵活方便。

总而言之:这真是极好的