16、LiveData的扩展函数:自定义LiveData扩展、LiveData的便捷操作、实战:倒计时LiveData
LiveData 本身已经很好用了,但说实话,在真实项目中我们经常需要重复写一些类似的逻辑。比如倒计时、网络状态监听、数据转换等等。这时候,扩展函数就成了我们的救星。
我个人习惯把 LiveData 的扩展函数单独放在一个文件里,叫 LiveDataExt.kt。这样团队里其他人一看就知道,哦,这里封装了 LiveData 的常用操作。今天我们就来聊聊怎么自定义 LiveData 扩展,以及一个非常实用的倒计时 LiveData 实战。
为什么需要扩展 LiveData?
你想想看,LiveData 本身只提供了 observe、postValue、setValue 这些基础方法。但在业务中,我们经常需要:
- 对数据进行映射转换
- 合并多个 LiveData
- 过滤掉重复数据
- 延迟执行某些操作
这些如果每次都手写,代码会变得又臭又长。扩展函数就是把这些通用逻辑抽出来,一次封装,到处使用。
自定义 LiveData 扩展:从简单到复杂
我们先从最简单的开始。假设你有一个 LiveData<String>,你想把它转成 LiveData<Int>。用 Transformations.map 当然可以,但每次都要写 lambda,有点啰嗦。
我自己封装了一个 mapToInt 扩展:
fun LiveData<String>.mapToInt(): LiveData<Int> {
return Transformations.map(this) { value ->
value.toIntOrNull() ?: 0
}
}
这样用起来就清爽多了:
val stringLiveData: LiveData<String> = ...
val intLiveData = stringLiveData.mapToInt()
嗯,这里要注意:扩展函数返回的是一个新的 LiveData,原来的 LiveData 不受影响。这是 LiveData 的设计哲学——不可变的数据流。
更实用的扩展:非空过滤
我在项目中遇到过一个问题:某个 LiveData 可能发射 null 值,但 UI 层只关心非空数据。每次在 Observer 里写 if (value != null) 真的很烦。
于是就有了这个扩展:
fun <T> LiveData<T?>.filterNotNull(): LiveData<T> {
return Transformations.map(this) { it }
.distinctUntilChanged()
}
等等,这里有个坑。直接用 Transformations.map 并不能过滤掉 null,因为 map 只是转换,不会跳过。正确的做法是用 MediatorLiveData:
fun <T> LiveData<T?>.filterNotNull(): LiveData<T> {
val mediator = MediatorLiveData<T>()
mediator.addSource(this) { value ->
if (value != null) {
mediator.value = value
}
}
return mediator
}
Transformations.map 做非空过滤,结果发现 UI 层还是收到了 null。因为 map 不会跳过源 LiveData 的发射,它只是把 null 转换成了另一个值。记住:Transformations.map 是一对一映射,不是过滤器。
LiveData 的便捷操作:合并与延迟
除了过滤,合并多个 LiveData 也是高频需求。比如你有两个 LiveData,一个代表用户名,一个代表用户头像 URL,你想合并成一个 User 对象。
Kotlin 的扩展函数让这件事变得很优雅:
fun <T1, T2, R> LiveData<T1>.combineWith(
other: LiveData<T2>,
transform: (T1, T2) -> R
): LiveData<R> {
val mediator = MediatorLiveData<R>()
var latestValue1: T1? = null
var latestValue2: T2? = null
mediator.addSource(this) { value1 ->
latestValue1 = value1
latestValue2?.let { v2 ->
mediator.value = transform(value1, v2)
}
}
mediator.addSource(other) { value2 ->
latestValue2 = value2
latestValue1?.let { v1 ->
mediator.value = transform(v1, value2)
}
}
return mediator
}
用起来就像这样:
val nameLiveData: LiveData<String> = ...
val avatarLiveData: LiveData<String> = ...
val userLiveData = nameLiveData.combineWith(avatarLiveData) { name, avatar ->
User(name, avatar)
}
说白了,这就是一个简易版的 LiveData<Pair> 合并器。当然,RxJava 的 combineLatest 更强大,但如果你整个项目只用 LiveData,这个扩展完全够用。
实战:倒计时 LiveData
好,重头戏来了。倒计时功能在项目中太常见了——验证码倒计时、活动倒计时、秒杀倒计时。每次都要写 Handler 或者 Timer,代码又散又乱。
我封装了一个 CountDownLiveData,它继承自 MutableLiveData,内部自动管理倒计时逻辑:
class CountDownLiveData(private val totalSeconds: Long) : MutableLiveData<Long>() {
private var countDownTimer: CountDownTimer? = null
fun start() {
// 如果已经在倒计时,先取消
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(totalSeconds * 1000, 1000) {
override fun onTick(millisUntilFinished: Long) {
val remainingSeconds = millisUntilFinished / 1000
postValue(remainingSeconds)
}
override fun onFinish() {
postValue(0L)
// 倒计时结束,自动清理
countDownTimer = null
}
}.start()
// 发射初始值
postValue(totalSeconds)
}
fun cancel() {
countDownTimer?.cancel()
countDownTimer = null
}
override fun onActive() {
super.onActive()
// 当有观察者时自动开始(可选)
// 我个人习惯手动调用 start(),更可控
}
override fun onInactive() {
super.onInactive()
// 没有观察者时自动取消,避免内存泄漏
cancel()
}
}
CountDownLiveData 在 onInactive() 时自动取消倒计时。这样当 Activity 或 Fragment 不可见时,倒计时不会白白跑着浪费资源。等用户回来,重新 observe 时,再手动 start() 即可。
使用起来非常简单:
// 在 ViewModel 中
val countDown = CountDownLiveData(60) // 60秒倒计时
fun startCountDown() {
countDown.start()
}
fun cancelCountDown() {
countDown.cancel()
}
// 在 Activity 中
viewModel.countDown.observe(this) { seconds ->
binding.tvCountDown.text = "剩余 ${seconds} 秒"
}
你看,ViewModel 里只需要两行代码就搞定了倒计时。而且因为 LiveData 的生命周期感知特性,倒计时不会在屏幕旋转时中断——当然,前提是你没有在 onInactive 里取消它。这里我故意留了个选择:你可以根据业务需求决定是否自动取消。
扩展函数的组织与命名规范
最后,聊聊我个人的组织习惯。扩展函数多了以后,管理起来也是个学问:
| 文件 | 内容 |
|---|---|
LiveDataExt.kt |
通用扩展:map、filter、combine 等 |
LiveDataTimer.kt |
时间相关:倒计时、延迟、轮询 |
LiveDataNetwork.kt |
网络相关:网络状态监听、请求结果封装 |
命名上,我建议用动词开头,比如 filterNotNull、combineWith、debounce。这样读起来就像在说「做什么操作」,语义清晰。
嗯,扩展函数虽然好用,但也不要滥用。如果一个扩展函数里逻辑超过 20 行,或者用到了多个 MediatorLiveData,我建议考虑封装成一个独立的 LiveData 子类。就像我们刚才的 CountDownLiveData 一样,可读性和可维护性都会更好。
好了,这一章的内容就到这里。扩展函数是 Kotlin 赋予我们的利器,用好了能让 LiveData 的状态管理如虎添翼。下一章我们会聊聊更高级的话题——LiveData 与协程的结合,敬请期待。