16、LiveData的扩展函数:自定义LiveData扩展、LiveData的便捷操作、实战:倒计时LiveData

LiveData 本身已经很好用了,但说实话,在真实项目中我们经常需要重复写一些类似的逻辑。比如倒计时、网络状态监听、数据转换等等。这时候,扩展函数就成了我们的救星。

我个人习惯把 LiveData 的扩展函数单独放在一个文件里,叫 LiveDataExt.kt。这样团队里其他人一看就知道,哦,这里封装了 LiveData 的常用操作。今天我们就来聊聊怎么自定义 LiveData 扩展,以及一个非常实用的倒计时 LiveData 实战。

为什么需要扩展 LiveData?

你想想看,LiveData 本身只提供了 observepostValuesetValue 这些基础方法。但在业务中,我们经常需要:

  • 对数据进行映射转换
  • 合并多个 LiveData
  • 过滤掉重复数据
  • 延迟执行某些操作

这些如果每次都手写,代码会变得又臭又长。扩展函数就是把这些通用逻辑抽出来,一次封装,到处使用。

核心思想: 扩展函数不是修改 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()
    }
}
设计思路: 我让 CountDownLiveDataonInactive() 时自动取消倒计时。这样当 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 网络相关:网络状态监听、请求结果封装

命名上,我建议用动词开头,比如 filterNotNullcombineWithdebounce。这样读起来就像在说「做什么操作」,语义清晰。

嗯,扩展函数虽然好用,但也不要滥用。如果一个扩展函数里逻辑超过 20 行,或者用到了多个 MediatorLiveData,我建议考虑封装成一个独立的 LiveData 子类。就像我们刚才的 CountDownLiveData 一样,可读性和可维护性都会更好。

总结一下: LiveData 扩展函数是 Kotlin 扩展特性在 Android 架构中的绝佳应用。它让我们用最少的代码,实现最通用的功能。从简单的 map 到复杂的倒计时,扩展函数让 LiveData 从一个基础组件变成了一个强大的状态管理工具。
LiveData 扩展函数知识体系 LiveData 扩展函数 基础扩展 mapToInt / filterNotNull distinctUntilChanged 非空过滤、类型转换 便捷操作 combineWith / merge debounce / throttle 合并、防抖、延迟 实战:倒计时 CountDownLiveData onActive / onInactive 自动取消、生命周期感知 核心原则 一次封装,到处使用 · 保持不可变 · 生命周期感知

好了,这一章的内容就到这里。扩展函数是 Kotlin 赋予我们的利器,用好了能让 LiveData 的状态管理如虎添翼。下一章我们会聊聊更高级的话题——LiveData 与协程的结合,敬请期待。

公众号:蓝海资料掘金营,微信 deep3321