7个Kotlin Delegate

概述

看这篇文章,了解7个delegate模式及对应代码,你可以直接复制使用。

在我们项目进行到第六个月时,我对代码库做了一次搜索。结果很震惊:

  • 847处null检查,用来检查"本应已初始化"的值
  • 34个手写的property change listener,写法各不相同
  • 12个验证逻辑,几乎完全重复

用了一周的时间,我把最严重的问题用property delegate重写了一遍。结果:

  • 删除了213行代码
  • 修复了隐藏在自定义listener中的2个bug
  • 代码审查时间从45分钟的激烈讨论变成了5分钟的快速批准

by关键字:开始之前

by 关键字的作用是:把property的读写操作交给一个delegate对象。这样你不用在每个类里重复写get/set代码,只需要写一次,然后在任何地方使用:

kotlin 复制代码
var name: String by SomeDelegate()

SomeDelegate 会拦截对这个property的所有读写操作。就是这么简单。下面介绍7个delegate,从你可能已经知道的开始。


1. lazy -- 需要时才初始化

lazy 的lambda只执行一次,在你第一次访问这个property时执行。它是线程安全的,执行后结果会被缓存。

不好的做法:

kotlin 复制代码
class ProfileViewModel : ViewModel() {
    private var _analytics: AnalyticsHelper? = null
    
    fun trackEvent(name: String) {
        if (_analytics == null) {
            _analytics = AnalyticsHelper.create()
        }
        _analytics!!.track(name)
    }
}

好的做法:

kotlin 复制代码
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。

不好的做法:

kotlin 复制代码
var username: String = ""
    set(value) {
        val old = field
        field = value
        onUsernameChanged(old, value)
    }

好的做法:

kotlin 复制代码
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 表示保持原值。

kotlin 复制代码
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() 提供相同的"先赋值再访问"的检查,但错误信息更清楚。

不好的做法:

kotlin 复制代码
lateinit var sessionId: String
// 访问前未赋值时:
// kotlin.UninitializedPropertyAccessException:
// lateinit property sessionId has not been initialized

好的做法:

kotlin 复制代码
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。不需要手动转换类型,也不需要写提取代码。

kotlin 复制代码
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> 接口,有两个方法:getValuesetValue。写一次后,所有使用这个delegate的property都会自动同步行为。

这是保存到SharedPreferences的例子:

kotlin 复制代码
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都变成了一行代码:

kotlin 复制代码
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。所有重复的 putStringgetString 代码都消失了。


7. 类Delegation -- 减少重复代码

这个用在类上,不是property。当你需要包装一个对象并添加一些行为时,Kotlin可以自动生成所有的转发代码。

不好的做法:

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
}

好的做法:

kotlin 复制代码
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行。

kotlin 复制代码
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 默认是线程安全的。但 observablevetoable 没有内置的同步机制。如果多个thread同时改一个 observable property,你需要自己加锁。

相关推荐
用户69371750013842 小时前
2026 Android 开发,现在还能入行吗?
android·前端·ai编程
YBZha2 小时前
Android Camera2 + OpenGL 竖屏或横屏预览会有“轻微拉伸”
android
seabirdssss3 小时前
Appium 在小米平板上的安装受限与闪退排查
android·appium·电脑
喂_balabala3 小时前
Kotlin-属性委托
android·开发语言·kotlin
空中海3 小时前
第一章:Android 系统架构与核心原理
android·系统架构
lI-_-Il4 小时前
适配工具箱:手机里的全能数字瑞士军刀
android·音视频
彳亍走的猪4 小时前
Android 全局防抖/防重复点击
android·java·开发语言
程序员陆业聪4 小时前
Android图片加载框架深度对比:Coil 3.4.0 vs Glide 5.0,该选哪个?
android
seabirdssss4 小时前
Android 模拟器搭建
android·经验分享