概述
看这篇文章,了解7个delegate模式及对应代码,你可以直接复制使用。
在我们项目进行到第六个月时,我对代码库做了一次搜索。结果很震惊:
- 847处null检查,用来检查"本应已初始化"的值
- 34个手写的property change listener,写法各不相同
- 12个验证逻辑,几乎完全重复
用了一周的时间,我把最严重的问题用property delegate重写了一遍。结果:
- 删除了213行代码
- 修复了隐藏在自定义listener中的2个bug
- 代码审查时间从45分钟的激烈讨论变成了5分钟的快速批准
by关键字:开始之前
by 关键字的作用是:把property的读写操作交给一个delegate对象。这样你不用在每个类里重复写get/set代码,只需要写一次,然后在任何地方使用:
var name: String by SomeDelegate()
SomeDelegate 会拦截对这个property的所有读写操作。就是这么简单。下面介绍7个delegate,从你可能已经知道的开始。
1. lazy -- 需要时才初始化
lazy 的lambda只执行一次,在你第一次访问这个property时执行。它是线程安全的,执行后结果会被缓存。
不好的做法:
class ProfileViewModel : ViewModel() {
private var _analytics: AnalyticsHelper? = null
fun trackEvent(name: String) {
if (_analytics == null) {
_analytics = AnalyticsHelper.create()
}
_analytics!!.track(name)
}
}
好的做法:
class ProfileViewModel : ViewModel() {
private val analytics by lazy {
AnalyticsHelper.create()
}
fun trackEvent(name: String) {
analytics.track(name)
}
}
AnalyticsHelper.create() 只在你第一次用 analytics 时才运行,之后结果被保存。null检查消失了,!! 操作符也没了。整个项目里我们删除了31个这样的模式。
2. observable -- 监听property变化
Delegates.observable 让你在property改变时执行代码,同时你能获取旧值和新值。不用写listener接口,也不用自己写setter。
不好的做法:
var username: String = ""
set(value) {
val old = field
field = value
onUsernameChanged(old, value)
}
好的做法:
import kotlin.properties.Delegates
var username: String by Delegates.observable("") { _, old, new ->
onUsernameChanged(old, new)
}
我们项目里有34个自定义setter,都在做同样的事,每个5-7行。这样就是170-238行代码被一个delegate替换了。ViewModel文件从冗长难读变成了清晰易懂,只用了一个下午。
3. vetoable -- 拒绝无效的值
Delegates.vetoable 类似 observable,但更严格。从lambda返回 true 表示接受新值,返回 false 表示保持原值。
import kotlin.properties.Delegates
var retryCount: Int by Delegates.vetoable(0) { _, _, new ->
new in 0..5
}
retryCount = 3 // 接受 - retryCount 变成 3
retryCount = 10 // 拒绝 - retryCount 保持 3
retryCount = -1 // 拒绝 - retryCount 保持 3
以前,我们的验证逻辑分散在三个地方:property的setter、ViewModel里的验证函数,还有Fragment里的条件判断。用 vetoable 后,所有逻辑都在一个地方。代码少了,逻辑也不会出现不一致。
4. notNull -- 更清楚的错误信息
lateinit 在访问未初始化的property时会抛出 UninitializedPropertyAccessException。但这个错误信息几乎没有用处,你看不出是哪个property出问题。Delegates.notNull() 提供相同的"先赋值再访问"的检查,但错误信息更清楚。
不好的做法:
lateinit var sessionId: String
// 访问前未赋值时:
// kotlin.UninitializedPropertyAccessException:
// lateinit property sessionId has not been initialized
好的做法:
import kotlin.properties.Delegates
var sessionId: String by Delegates.notNull()
// 访问前未赋值时:
// java.lang.IllegalStateException:
// Property sessionId should be initialized before get.
5. Map delegation -- 简化配置读取
如果你的数据是 Map<String, Any> 的形式,可以直接把property关联到这个map。property的名字就是map的key。不需要手动转换类型,也不需要写提取代码。
class ServerConfig(private val map: Map<String, Any>) {
val host: String by map
val port: Int by map
val timeout: Long by map
}
val config = ServerConfig(
mapOf(
"host" to "api.example.com",
"port" to 443,
"timeout" to 30_000L
)
)
println(config.host) // api.example.com
println(config.port) // 443
println(config.timeout) // 30000
我们用3行property声明替换了80行配置解析代码。这个方法对JSON反序列化也特别有用,只要JSON的key和property名一致就行。
6. 自定义Delegate -- 写一次用到处
如果没有现成的delegate符合你的需求,可以自己写。实现 ReadWriteProperty<Any?, T> 接口,有两个方法:getValue 和 setValue。写一次后,所有使用这个delegate的property都会自动同步行为。
这是保存到SharedPreferences的例子:
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SharedPrefDelegate(
private val prefs: SharedPreferences,
private val key: String,
private val default: String
) : ReadWriteProperty<Any?, String> {
override fun getValue(thisRef: Any?, property: KProperty<*>): String =
prefs.getString(key, default) ?: default
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
prefs.edit().putString(key, value).apply()
}
}
现在所有SharedPreferences的property都变成了一行代码:
class SettingsRepository(prefs: SharedPreferences) {
var theme by SharedPrefDelegate(prefs, "theme", "light")
var language by SharedPrefDelegate(prefs, "language", "en")
var userId by SharedPrefDelegate(prefs, "user_id", "")
}
settings.theme = "dark"
// 自动保存到SharedPreferences
println(settings.theme)
// 自动从SharedPreferences读取
写一次,用到20个property。所有重复的 putString 和 getString 代码都消失了。
7. 类Delegation -- 减少重复代码
这个用在类上,不是property。当你需要包装一个对象并添加一些行为时,Kotlin可以自动生成所有的转发代码。
不好的做法:
class LoggingList<T>(private val inner: MutableList<T>) : MutableList<T> {
override fun add(element: T): Boolean {
println("Adding: $element")
return inner.add(element)
}
override fun remove(element: T): Boolean = inner.remove(element)
override fun contains(element: T): Boolean = inner.contains(element)
override fun size(): Int = inner.size()
// ... 还有14个方法,只是简单转发给inner
}
好的做法:
class LoggingList<T>(private val inner: MutableList<T>) : MutableList<T> by inner {
override fun add(element: T): Boolean {
println("Adding: $element")
return inner.add(element)
}
// 其他所有方法自动生成,只用override你想改的方法
}
by inner 告诉编译器:"我没override的方法都转发给inner"。那14个只为了满足interface而写的转发方法就消失了。
实际例子:一个真实的ViewModel
这是我们上季度重构的ViewModel,简化版。重构前是94行,重构后是32行。
class UserProfileViewModel(
private val prefs: SharedPreferences
) : ViewModel() {
// 只在用户打开profile页面时才创建
private val analytics by lazy {
AnalyticsService.create()
}
// 记住上次查看的user ID,重启app后不会丢失
var lastViewedUserId by SharedPrefDelegate(prefs, "last_user_id", "")
// 显示名字改变时自动更新UI
var displayName: String by Delegates.observable("") { _, _, new ->
updateDisplayNameInUI(new)
}
// follower数不会低于0,即使server返回负数
var followerCount: Int by Delegates.vetoable(0) { _, _, new ->
new >= 0
}
}
四个问题,四个delegate解决,四行代码搞定。
两个常见的坑
第一个坑: 不要用 lazy 来处理可变的state。lazy 是为 val 设计的,它只计算一次。如果你需要一个property延迟初始化但之后可以改变,应该自己写 ReadWriteProperty。
第二个坑: thread safety。lazy 默认是线程安全的。但 observable 和 vetoable 没有内置的同步机制。如果多个thread同时改一个 observable property,你需要自己加锁。