在Android开发中,数据的动态更新一直是个让人头疼的问题。想象一下:你的界面需要实时显示用户的余额变化,或者一个聊天应用的未读消息数得随时刷新。过去,我们可能会用Handler、手动监听器,或者一堆回调来搞定这些需求,但结果往往是代码乱如麻,维护起来像在拆炸弹。LiveData的出现,就是为了解决这些痛点。
LiveData是Android Jetpack提供的一种观察者模式的实现,专为生命周期感知而生。它的核心卖点有三:
生命周期安全:LiveData知道Activity或Fragment的生命周期状态,避免了在界面销毁后还尝试更新UI导致的崩溃。
数据驱动UI:数据一变,UI自动更新,省去你手动通知的麻烦。
简洁优雅:几行代码就能实现复杂的数据-视图绑定,妈妈再也不用担心我的回调地狱了!
场景举例:假设你正在开发一个天气应用,用户切换城市后,界面需要立刻显示新城市的气温、湿度等信息。如果用传统方式,你可能需要写一堆异步任务、回调,还得小心Activity销毁时的内存泄漏。用LiveData?几行代码,数据和UI自动同步,丝滑得像在用魔法。
在动手写代码之前,咱们先把LiveData的“灵魂”搞清楚。LiveData本质上是一个数据持有者,它允许你把数据“装”进去,然后让其他组件(比如UI)订阅这些数据的变化。听起来有点像RxJava的Observable?但LiveData更轻量,且天然为Android生命周期优化。
生命周期感知:LiveData只在观察者(Observer)处于活跃状态(STARTED或RESUMED)时发送更新,Activity暂停或销毁时自动停止通知。
粘性事件:新订阅者会收到最近一次的数据(如果有),非常适合恢复UI状态。
线程安全:LiveData可以在主线程或子线程更新数据,但UI更新一定在主线程,省心省力。
与ViewModel强强联合:LiveData通常搭配ViewModel使用,完美适配MVVM架构。
LiveData有几个常见的变体,了解它们能帮你更灵活地应对需求:
MutableLiveData:可变的LiveData,允许你直接更新数据。日常开发中最常用。
LiveData:只读的基类,通常用来暴露数据给观察者,防止外部随意篡改。
MediatorLiveData:高级玩家专用,可以监听多个LiveData,合并数据后再通知观察者。
小贴士:如果你只是想快速实现数据到UI的绑定,99%的情况下用MutableLiveData就够了。但如果要玩花活儿,比如监听两个数据源的变化再决定UI怎么更新,MediatorLiveData会是你的好朋友。
下面是一个简单的例子:一个计数器应用,用户点击按钮,计数增加,界面实时显示最新计数。咱们用LiveData来实现这个功能。
确保你的build.gradle(Module级别)里加了Jetpack的依赖:
dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.6"
}
注意:本文用的是2.8.6版本(截至2025年6月最新),建议检查官方文档确保版本最新。
ViewModel是LiveData的“最佳拍档”,负责持有数据和业务逻辑。我们先创建一个简单的CounterViewModel:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0) // 初始值为0
val count: LiveData get() = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
代码解析:
_count 是 MutableLiveData,用来在ViewModel内部更新数据。
count 是只读的 LiveData,暴露给外部观察者,防止数据被随意修改。
increment() 方法更新计数,value 属性用于设置新值。
为什么用下划线 _count? 这是Kotlin社区的惯例,私有可变字段用下划线前缀,只读公开字段用普通命名。这样既清晰又安全,强烈推荐!
接下来,在Activity里设置UI并观察LiveData的变化:
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 观察LiveData
viewModel.count.observe(this, Observer { count ->
textView.text = "Count: $count"
})
// 按钮点击增加计数
button.setOnClickListener {
viewModel.increment()
}
}
}
对应的布局文件 activity_main.xml:
运行代码后,点击按钮,计数器会增加,TextView实时更新显示“Count: 1”、“Count: 2”……是不是超级简单?更重要的是,LiveData帮你自动处理了生命周期,Activity销毁后不会引发崩溃,屏幕旋转后数据也能自动恢复。
光会用还不够,咱们得搞清楚LiveData是怎么做到这么“聪明”的。以下是LiveData的核心工作机制:
LiveData基于观察者模式,核心是observe方法:
liveData.observe(lifecycleOwner, observer)
lifecycleOwner:通常是Activity或Fragment,告诉LiveData你的生命周期状态。
observer:当数据变化时,LiveData会调用observer.onChanged()通知更新。
LiveData内部通过LifecycleRegistry监听lifecycleOwner的状态。只有当生命周期处于STARTED或RESUMED时,onChanged()才会被调用。这意味着:
如果Activity暂停(比如屏幕关闭),LiveData不会发送更新,节省资源。
如果Activity销毁,LiveData会自动移除观察者,避免内存泄漏。
LiveData的“粘性”特性是它的杀手锏之一。新订阅者会立即收到最近一次的数据。这在以下场景特别有用:
屏幕旋转:Activity重建后,UI能立刻恢复到之前的状态。
延迟加载:比如Fragment在ViewPager中切换回来,依然能拿到最新的数据。
注意:粘性事件有时会带来副作用,比如你不希望新观察者收到旧数据。别急,后续会讲到SingleLiveEvent等解决方案。
LiveData的value属性只能在主线程设置,否则会抛异常。如果需要在子线程更新数据,用postValue:
MutableLiveData().postValue("Data from background thread")
postValue会将更新任务发送到主线程的消息队列,确保UI更新安全。
理论讲得差不多了,咱们来点更实用的!假设你在开发一个应用,需要从网络获取用户资料并显示到界面上。LiveData可以让这个过程变得优雅无比。
我们要从API获取用户信息(比如用户名和邮箱),然后显示到界面。如果网络失败,显示错误提示。用户点击“重试”按钮可以重新请求。
先创建一个简单的User数据类:
data class User(val username: String, val email: String)
我们用Retrofit(或其他网络库)来模拟网络请求,ViewModel负责处理数据逻辑:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class UserViewModel : ViewModel() {
private val _user = MutableLiveData()
val user: LiveData get() = _user
private val _error = MutableLiveData()
val error: LiveData get() = _error
fun fetchUser() {
viewModelScope.launch {
try {
// 模拟网络请求
val response = apiService.getUser()
_user.value = response
} catch (e: Exception) {
_error.value = "Failed to load user: ${e.message}"
}
}
}
}
代码解析:
用viewModelScope启动协程,确保网络请求在ViewModel销毁时自动取消。
_user和_error分别存储用户数据和错误信息,暴露为只读LiveData。
fetchUser()触发网络请求,成功时更新_user,失败时更新_error。
在Activity中观察user和error的变化:
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_user.*
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_user)
// 观察用户数据
viewModel.user.observe(this) { user ->
usernameTextView.text = user.username
emailTextView.text = user.email
}
// 观察错误信息
viewModel.error.observe(this) { error ->
errorTextView.text = error
}
// 点击重试
retryButton.setOnClickListener {
viewModel.fetchUser()
}
// 初次加载
viewModel.fetchUser()
}
}
对应的布局activity_user.xml:
为了简单起见,我们用一个假的apiService模拟网络请求:
object ApiService {
suspend fun getUser(): User {
// 模拟网络延迟
delay(1000)
// 随机模拟成功或失败
if (Random.nextBoolean()) {
return User("Grok", "[email protected]")
} else {
throw Exception("Network error")
}
}
}
运行后,点击“Retry”按钮会触发网络请求。如果成功,界面显示用户名和邮箱;如果失败,显示错误信息。整个过程完全生命周期安全,而且代码清晰,维护起来简直不要太爽!
小技巧:你可以用LiveData
>来包装错误信息,确保错误只被消费一次(避免重复显示Toast)。这在Part 2会详细讲解!
LiveData虽然好用,但也有几个容易踩的坑。以下是开发中常见的“翻车”场景和应对方法:
问题:新Fragment加入ViewPager后,收到旧数据,导致UI显示错误。 解决:用SingleLiveEvent(自定义类)或LiveData的observeForever结合手动移除观察者。代码如下:
class SingleLiveEvent : MutableLiveData() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer) {
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
}
问题:在子线程直接调用setValue会崩溃。 解决:用postValue替代,或者用协程切换到主线程:
viewModelScope.launch(Dispatchers.Main) {
_data.value = newValue
}
问题:忘记移除观察者导致泄漏。 解决:好消息!用observe(lifecycleOwner, observer)时,LiveData会自动管理观察者的生命周期,无需手动移除。但如果你用了observeForever,记得在合适时机调用removeObserver。
在Part 1中,我们用MutableLiveData轻松实现了数据到UI的绑定。但现实开发中,需求往往没那么简单。比如,你需要监听多个数据源的变化,然后根据它们的组合状态更新UI。这时候,MediatorLiveData 就派上用场了!它就像LiveData家族的“指挥家”,能协调多个LiveData,合并数据后再通知观察者。
MediatorLiveData 是 LiveData 的子类,允许你监听多个 LiveData 源,并在它们变化时动态处理数据。它特别适合以下场景:
合并多个网络请求的结果。
根据多个条件决定UI的显示状态(比如表单验证)。
实现复杂的数据依赖逻辑。
场景举例:想象一个电商应用的商品详情页,价格需要结合基础价格和优惠券折扣两个数据源计算最终显示价格。如果用普通LiveData,你可能得写一堆回调逻辑,用MediatorLiveData?几行代码搞定!
咱们来实现一个简单的例子:用户查看商品详情,界面显示商品的最终价格(基础价格 - 折扣)。基础价格和折扣分别来自两个LiveData。
以下是PriceViewModel,用MediatorLiveData合并两个数据源:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class PriceViewModel : ViewModel() {
// 基础价格(模拟从服务器获取)
private val _basePrice = MutableLiveData(100.0)
val basePrice: LiveData get() = _basePrice
// 折扣(模拟用户选择的优惠券)
private val _discount = MutableLiveData(20.0)
val discount: LiveData get() = _discount
// 最终价格
private val _finalPrice = MediatorLiveData()
val finalPrice: LiveData get() = _finalPrice
init {
// 监听basePrice和discount的变化
_finalPrice.addSource(_basePrice) { base ->
updateFinalPrice(base, _discount.value)
}
_finalPrice.addSource(_discount) { discount ->
updateFinalPrice(_basePrice.value, discount)
}
}
private fun updateFinalPrice(base: Double?, discount: Double?) {
if (base != null && discount != null) {
_finalPrice.value = base - discount
}
}
// 模拟更新数据
fun updateBasePrice(newPrice: Double) {
_basePrice.value = newPrice
}
fun updateDiscount(newDiscount: Double) {
_discount.value = newDiscount
}
}
代码解析:
_basePrice 和 _discount 是两个独立的数据源。
_finalPrice 是 MediatorLiveData,通过 addSource 监听 _basePrice 和 _discount 的变化。
updateFinalPrice 确保只有当两个数据都非空时才计算最终价格,避免空指针问题。
在Activity中观察 finalPrice 并显示结果:
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_price.*
class PriceActivity : AppCompatActivity() {
private val viewModel: PriceViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_price)
// 观察最终价格
viewModel.finalPrice.observe(this) { price ->
priceTextView.text = "Final Price: $${price:.2f}"
}
// 模拟价格变化
updatePriceButton.setOnClickListener {
viewModel.updateBasePrice((100..150).random().toDouble())
}
// 模拟折扣变化
updateDiscountButton.setOnClickListener {
viewModel.updateDiscount((10..30).random().toDouble())
}
}
}
布局文件 activity_price.xml:
运行后,点击“Update Base Price”或“Update Discount”,finalPrice 会自动重新计算,界面实时显示最新价格。整个过程优雅且高效,完全不用担心生命周期问题!
小技巧:用 MediatorLiveData 时,记得在 addSource 的回调中检查数据是否有效(比如非空),否则可能导致意外的UI更新。
除了合并数据,MediatorLiveData 还能干啥?以下是几个高级场景:
动态添加/移除数据源:用 removeSource 动态管理监听,适合复杂的状态切换。
复杂逻辑处理:比如监听三个LiveData,只有当所有条件都满足时才更新UI。
结合协程:在 viewModelScope 中异步处理数据,再通过 MediatorLiveData 通知结果。
彩蛋:试试用 MediatorLiveData 实现一个表单验证逻辑,比如监听用户名、密码、邮箱三个输入框的LiveData,只有当所有输入都合法时才启用“提交”按钮。代码留给你练手,Part 3 会给参考答案!
有时候,你不想直接把LiveData的数据丢给UI,而是需要先加工一下。比如,把价格从Double转成格式化的字符串,或者把用户列表过滤出VIP用户。这时候,Transformations 就派上用场了!
Transformations 是 LiveData 提供的工具类,包含两个主要方法:
map:对LiveData的数据进行转换,生成新的LiveData。
switchMap:根据LiveData的值动态切换另一个LiveData。
咱们基于上面的价格例子,用 Transformations.map 把 finalPrice 转成格式化的字符串:
在 PriceViewModel 中添加转换逻辑:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.Transformations
class PriceViewModel : ViewModel() {
private val _basePrice = MutableLiveData(100.0)
val basePrice: LiveData get() = _basePrice
private val _discount = MutableLiveData(20.0)
val discount: LiveData get() = _discount
private val _finalPrice = MediatorLiveData()
val finalPrice: LiveData get() = _finalPrice
// 新增:格式化的价格
val formattedPrice: LiveData = Transformations.map(_finalPrice) { price ->
String.format("Final Price: $%.2f", price)
}
init {
_finalPrice.addSource(_basePrice) { base ->
updateFinalPrice(base, _discount.value)
}
_finalPrice.addSource(_discount) { discount ->
updateFinalPrice(_basePrice.value, discount)
}
}
private fun updateFinalPrice(base: Double?, discount: Double?) {
if (base != null && discount != null) {
_finalPrice.value = base - discount
}
}
fun updateBasePrice(newPrice: Double) {
_basePrice.value = newPrice
}
fun updateDiscount(newDiscount: Double) {
_discount.value = newDiscount
}
}
直接观察 formattedPrice:
viewModel.formattedPrice.observe(this) { formattedPrice ->
priceTextView.text = formattedPrice
}
现在,priceTextView 直接显示格式化的字符串,比如“Final Price: $80.00”。代码更简洁,UI逻辑完全交给ViewModel,符合MVVM的理念!
Transformations.switchMap 更强大,适合动态切换数据源。比如,用户选择不同城市,LiveData动态加载对应城市的天气数据。
假设我们有一个下拉框让用户选择城市,ViewModel根据选择的城市加载天气数据。
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.Transformations
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
data class Weather(val city: String, val temperature: Int)
class WeatherViewModel : ViewModel() {
// 用户选择的城市
private val _selectedCity = MutableLiveData()
val selectedCity: LiveData get() = _selectedCity
// 天气数据
val weather: LiveData = Transformations.switchMap(_selectedCity) { city ->
MutableLiveData().apply {
fetchWeather(city)
}
}
fun selectCity(city: String) {
_selectedCity.value = city
}
private fun fetchWeather(city: String) {
viewModelScope.launch {
// 模拟网络请求
delay(500)
(weather as MutableLiveData).value = Weather(city, (10..30).random())
}
}
}
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_weather.*
class WeatherActivity : AppCompatActivity() {
private val viewModel: WeatherViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
// 观察天气数据
viewModel.weather.observe(this) { weather ->
weatherTextView.text = "${weather.city}: ${weather.temperature}°C"
}
// 模拟选择城市
selectCityButton.setOnClickListener {
val cities = listOf("Beijing", "Shanghai", "Guangzhou")
viewModel.selectCity(cities.random())
}
}
}
布局 activity_weather.xml:
点击按钮随机选择城市,weatherTextView 会显示对应城市的天气。switchMap 确保每次城市变化时,LiveData动态切换到新的数据源,代码简洁且响应迅速!
注意:switchMap 返回的 LiveData 必须是新的实例,不能复用同一个 MutableLiveData,否则可能导致数据混乱。
在Part 1中,我们提到LiveData的粘性事件(新观察者收到最近一次数据)在某些场景下会引发问题。比如,一个对话框只应该在特定事件触发时弹出,但因为粘性事件,新创建的Fragment可能会错误地弹出旧对话框。
Part 1已经给出了SingleLiveEvent的实现,这里再贴一遍,方便参考:
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
class SingleLiveEvent : MutableLiveData() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer) {
super.observe(owner) { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
}
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
}
使用方式:
class DialogViewModel : ViewModel() {
private val _showDialog = SingleLiveEvent()
val showDialog: LiveData get() = _showDialog
fun triggerDialog() {
_showDialog.value = "Hello, Dialog!"
}
}
在Activity:
viewModel.showDialog.observe(this) { message ->
// 显示对话框
}
SingleLiveEvent 解决了粘性问题,但它有局限性,比如不支持postValue,也不适合多观察者场景。
更现代的做法是用Event类包装数据,确保事件只被消费一次:
class Event(private val content: T) {
private var hasBeenHandled = false
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
}
ViewModel:
class DialogViewModel : ViewModel() {
private val _showDialog = MutableLiveData()
val showDialog: LiveData get() = _showDialog
fun triggerDialog() {
_showDialog.value = Event("Hello, Dialog!")
}
}
Activity:
viewModel.showDialog.observe(this) { event ->
event.getContentIfNotHandled()?.let { message ->
// 显示通知框
}
}
优点:简单、灵活,支持postValue,代码可读性强。 缺点:**:比SingleLiveEvent更通用,社区广泛使用,推荐!
小贴士:如果你的项目用Kotlin Flow,可以用SharedFlow 替代Event,更现代。Part 4会详细对比LiveData和Flow。
想知道LiveData为什么这么聪明??我们来瞅瞅它的源码(基于androidx.lifecycle 2.8.6)。以下是几个核心部分的简析:
@Override
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) {
assertMainThread("observe");
if (owner.getLifecycle().getCurrentState() == DESTROYED) {
return;
}
LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
if (existing != null && existing.isBoundToDifferentOwner()) {
throw new IllegalArgumentException("Cannot add observer twice...");
}
owner.getLifecycle().addObserver(wrapper);
}
解析:
assertMainThread 确保observe 在主线程调用。
LifecycleBoundObserver 是内部包装类,负责监听生命周期变化。
mObservers 存储所有观察者,用 putIfAbsent 防止重复添加。
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
void dispatchingValue(@Nullable ObserverWrapper initiator) {
if (mDispatchingValue) {
mDispatchInvalidated = true;
return;
}
mDispatchingValue = true;
try {
Iterator> iterator = mObservers.iteratorWithAdditions();
while (iterator.hasNext()) {
considerNotify(iterator.next().getValue());
if (mDispatchInvalidated) {
mDispatchInvalidated = false;
}
}
} finally {
mDispatchingValue = false;
}
}
解析:
setValue 更新mData 并触发dispatchingValue。
considerNotify 检查观察者的生命周期状态,只有活跃状态才调用onChanged。
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
observer.mObserver.onChanged((T) mData);
}
解析:
mData 存储最新数据,新观察者通过onChanged 立即收到。
这就是粘性事件的实现:只要有数据,新观察者都会收到。
启发:LiveData的源码并不复杂,核心逻辑集中在LiveData.java(约1000行)。建议抽空翻翻,理解它的观察者管理和生命周期处理,对写自定义响应式组件很有帮助!
用户快速点击按钮可能触发多次网络请求,怎么防抖?用LiveData+coroutine`:
class SearchViewModel : ViewModel() {
private val _searchQuery = MutableLiveData()
val searchQuery : LiveData get() = _searchQuery
val searchResult : LiveData = Transformations.switchMap(_searchQuery) { query ->
liveData(viewModelScope) {
delay(500) // 防抖 0.5秒
emit(fetchSearchResult(query))
}
}
fun search(query: String) {
_searchQuery.value = query
}
}
Room天然支持LiveData,查询方法加@Query 注解即可:
@Dao
interface UserDao {
fun @Query("SELECT * FROM users")
fun getUsers(): LiveData>
}
class UserRepository(private val dao: UserDao) {
val users: LiveData> = dao.getUsers()
}
在Part 1的网络请求中,扩展为带重试次数的逻辑:
class RetryViewModel : ViewModel() {
private val _data = MutableLiveData()
val data: LiveData get() = _data
private val _error = MutableLiveData()
val error: LiveData get() = _error
private var retryCount = 0
fun fetchWithRetry() {
viewModelScope.launch {
try {
_data.value = apiService.getData()
retryCount = 0
} catch (e: Exception) {
retryCount++
if (retryCount < 3) {
_error.value = "Retrying... ($retryCount/3)"
fetchWithRetry()
} else {
_error.value = "Failed after 3 retries: ${e.message}"
}
}
}
}
}
随着Kotlin的普及,StateFlow 和 SharedFlow 逐渐成为响应式编程的新宠。很多人会问:LiveData还有必要学吗?它跟Flow比谁更香? 别急,咱们来一场硬核对比,帮你搞清楚两者的优劣和适用场景!
特性 |
LiveData |
Kotlin Flow |
---|---|---|
生命周期感知 |
内置,自动处理Activity/Fragment生命周期 |
无,需要手动绑定生命周期(如lifecycleScope) |
粘性事件 |
默认支持,新观察者收到最近数据 |
无,需用StateFlow或自定义实现 |
线程支持 |
setValue主线程,postValue子线程 |
灵活,支持任何协程调度器 |
操作符丰富度 |
有限(map、switchMap等) |
超丰富(map、filter、combine等) |
冷/热流 |
热流,始终保持最新值 |
冷流(默认),StateFlow/SharedFlow为热流 |
学习曲线 |
简单,Android开发者上手快 |
稍陡,需要了解协程和Flow操作符 |
LiveData的优点:
开箱即用:专为Android设计,生命周期管理无脑省心。
粘性事件:适合UI状态恢复,比如屏幕旋转后自动恢复数据。
轻量:API简单,适合中小型项目或快速开发。
Flow的优点:
灵活性:支持复杂的操作链,比如过滤、合并、去重等。
协程友好:无缝集成viewModelScope或lifecycleScope。
跨平台潜力:Flow是Kotlin标准库的一部分,不依赖Android。
痛点对比:
LiveData:粘性事件可能引发意外UI更新,操作符较少,难以处理复杂数据流。
Flow:需要手动管理生命周期,可能增加代码复杂度。
用LiveData:快速开发、UI驱动的场景(比如表单、列表展示),或者你想最大程度利用Jetpack生态。
用Flow:复杂数据处理(比如多数据源合并、实时搜索防抖),或你的项目已经全面拥抱协程。
混合使用:在ViewModel中用Flow处理复杂逻辑,转换为LiveData暴露给UI,兼顾两者的优点。
假设我们要实现一个实时搜索功能,用户输入查询词,ViewModel从网络获取结果,延迟500ms防抖。咱们用Flow处理逻辑,再转成LiveData给UI。
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
val query: StateFlow get() = _query
val searchResult: LiveData = _query
.debounce(500) // 防抖0.5秒
.filter { it.isNotEmpty() } // 过滤空查询
.mapLatest { fetchSearchResult(it) } // 获取最新结果
.asLiveData() // 转为LiveData
fun setQuery(newQuery: String) {
_query.value = newQuery
}
private suspend fun fetchSearchResult(query: String): String {
// 模拟网络请求
delay(500)
return "Result for: $query"
}
}
代码解析:
_query 是 MutableStateFlow,用来接收用户输入。
debounce 和 mapLatest 是Flow的强大操作符,分别实现防抖和取最新结果。
asLiveData() 将Flow转为LiveData,自动绑定ViewModel的生命周期。
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_search.*
class SearchActivity : AppCompatActivity() {
private val viewModel: SearchViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
// 观察搜索结果
viewModel.searchResult.observe(this) { result ->
resultTextView.text = result
}
// 用户输入
searchEditText.addTextChangedListener { text ->
viewModel.setQuery(text.toString())
}
}
}
布局 activity_search.xml:
用户输入查询词,500ms后界面显示搜索结果。Flow的debounce确保快速输入不会触发多次请求,asLiveData()保证生命周期安全,整个流程丝滑无比!
小技巧:如果你的项目已经全面用协程,可以直接用StateFlow给UI,省去转LiveData的步骤。但如果需要兼容老代码,混合使用是最佳选择。
LiveData虽然好用,但用不好也可能导致性能问题,比如频繁更新导致UI卡顿,或者内存占用过高。下面是几个优化技巧,帮你让LiveData跑得更快、更省资源!
LiveData默认会通知所有数据变化,即使新旧值相同。可以用Transformations.distinctUntilChanged过滤重复数据:
val optimizedData: LiveData = Transformations.distinctUntilChanged(liveData)
场景:用户列表更新时,只有当列表内容真的变化时才刷新RecyclerView,避免无意义的UI重绘。
如果LiveData的数据初始化成本高(比如数据库查询),可以用lazy或条件触发:
class LazyViewModel : ViewModel() {
private val _data by lazy { MutableLiveData() }
val data: LiveData get() = _data
fun loadData() {
viewModelScope.launch {
_data.value = fetchExpensiveData()
}
}
}
频繁调用setValue可能触发多次UI更新,影响性能。可以用MediatorLiveData合并多次更新:
class BatchViewModel : ViewModel() {
private val _batchData = MediatorLiveData>()
val batchData: LiveData> get() = _batchData
private val sources = mutableListOf>()
fun addSource(source: LiveData) {
sources.add(source)
_batchData.addSource(source) { value ->
val currentList = _batchData.value?.toMutableList() ?: mutableListOf()
currentList.add(value)
_batchData.value = currentList
}
}
}
不要在每个Fragment都观察同一个LiveData,尽量在Activity或父Fragment中统一管理,减少观察者数量。
彩蛋:用Android Studio的Profiler工具监控LiveData的更新频率,如果发现频繁触发onChanged,检查是否有多余的setValue调用。
写代码不测试,等于开车不系安全带!LiveData的测试需要一些技巧,因为它涉及生命周期和主线程。下面是测试LiveData的正确姿势。
在build.gradle中添加测试依赖:
dependencies {
testImplementation "junit:junit:4.13.2"
testImplementation "androidx.arch.core:core-testing:2.2.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1"
}
假设我们要测试前面的CounterViewModel:
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
class CounterViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule() // 让LiveData在测试中同步执行
private val viewModel = CounterViewModel()
private val observer: Observer = mock()
@Test
fun `increment should increase count`() {
// Arrange
viewModel.count.observeForever(observer)
// Act
viewModel.increment()
// Assert
verify(observer).onChanged(1)
}
}
解析:
InstantTaskExecutorRule 让LiveData的setValue同步执行,适合测试。
observeForever 用于测试,因为单元测试没有LifecycleOwner。
用Mockito验证观察者收到正确的值。
如果ViewModel用了协程(比如Part 2的网络请求),需要coroutines-test:
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
class UserViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
@Test
fun `fetchUser should update user LiveData`() = runBlockingTest {
// Arrange
val viewModel = UserViewModel()
val observer: Observer = mock()
viewModel.user.observeForever(observer)
// Act
viewModel.fetchUser()
// Assert
verify(observer).onChanged(User("Grok", "[email protected]"))
}
}
注意:测试时记得清理协程环境,避免干扰其他测试。
最后,咱们来个硬核实战:一个Todo应用的LiveData实现,涉及多模块、Room数据库和MVVM架构。
app/
├── data/
│ ├── db/
│ │ ├── TodoDao.kt
│ │ ├── TodoDatabase.kt
│ ├── repository/
│ │ ├── TodoRepository.kt
├── ui/
│ ├── todo/
│ │ ├── TodoViewModel.kt
│ │ ├── TodoFragment.kt
│ │ ├── todo_fragment.xml
Todo.kt:
@Entity(tableName = "todos")
data class Todo(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val isCompleted: Boolean
)
TodoDao.kt:
@Dao
interface TodoDao {
@Query("SELECT * FROM todos")
fun getAllTodos(): LiveData>
@Insert
suspend fun insert(todo: Todo)
}
TodoDatabase.kt:
@Database(entities = [Todo::class], version = 1)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
}
TodoRepository.kt:
class TodoRepository(private val dao: TodoDao) {
val todos: LiveData> = dao.getAllTodos()
suspend fun addTodo(title: String) {
dao.insert(Todo(title = title, isCompleted = false))
}
}
TodoViewModel.kt:
class TodoViewModel(private val repository: TodoRepository) : ViewModel() {
val todos: LiveData> = repository.todos
private val _error = MutableLiveData()
val error: LiveData get() = _error
fun addTodo(title: String) {
viewModelScope.launch {
if (title.isBlank()) {
_error.value = "Title cannot be empty!"
} else {
repository.addTodo(title)
}
}
}
}
TodoFragment.kt:
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.todo_fragment.*
class TodoFragment : Fragment(R.layout.todo_fragment) {
private val viewModel: TodoViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置RecyclerView
todoRecyclerView.layoutManager = LinearLayoutManager(context)
val adapter = TodoAdapter()
todoRecyclerView.adapter = adapter
// 观察todos
viewModel.todos.observe(viewLifecycleOwner) { todos ->
adapter.submitList(todos)
}
// 观察错误
viewModel.error.observe(viewLifecycleOwner) { error ->
errorTextView.text = error
}
// 添加Todo
addButton.setOnClickListener {
viewModel.addTodo(todoEditText.text.toString())
todoEditText.text.clear()
}
}
}
TodoAdapter.kt:
class TodoAdapter : ListAdapter(TodoDiffCallback()) {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val titleTextView: TextView = view.findViewById(android.R.id.text1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val todo = getItem(position)
holder.titleTextView.text = todo.title
}
}
class TodoDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Todo, newItem: Todo): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Todo, newItem: Todo): Boolean = oldItem == newItem
}
todo_fragment.xml:
运行后,用户输入Todo标题,点击“Add”按钮,列表实时更新。如果标题为空,显示错误提示。Room的LiveData支持确保数据自动同步,生命周期安全,体验丝滑!
LiveData虽然强大,但有时候内置的功能没法完全满足需求。比如,你想实现一个 定时刷新 的LiveData,或者一个 基于传感器数据 的LiveData。这时候,自定义LiveData就派上用场了!通过继承 LiveData,你可以打造出完全符合业务需求的“超级英雄”。
LiveData的核心是:
数据存储:通过 setValue 或 postValue 更新数据。
观察者管理:通过 onActive 和 onInactive 感知观察者的活跃状态。
生命周期绑定:自动处理观察者的添加和移除。
自定义LiveData只需要重写关键方法,比如 onActive(当有活跃观察者时调用)和 onInactive(当所有观察者不活跃时调用)。
咱们来实现一个 TimerLiveData,每秒更新当前时间,供UI显示(比如一个实时钟表)。
import androidx.lifecycle.LiveData
import java.util.Timer
import java.util.TimerTask
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class TimerLiveData : LiveData() {
private var timer: Timer? = null
private val formatter = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
override fun onActive() {
// 有活跃观察者时启动定时器
timer = Timer().apply {
scheduleAtFixedRate(object : TimerTask() {
override fun run() {
postValue(formatter.format(Date()))
}
}, 0, 1000)
}
}
override fun onInactive() {
// 没有活跃观察者时取消定时器
timer?.cancel()
timer = null
}
}
代码解析:
onActive:当UI开始观察时,启动一个每秒更新的定时器,通过 postValue 发送格式化时间。
onInactive:当UI暂停或销毁时,取消定时器,释放资源。
用 postValue 确保线程安全,因为 Timer 运行在子线程。
在ViewModel中:
class ClockViewModel : ViewModel() {
val currentTime: LiveData = TimerLiveData()
}
在Activity中:
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_clock.*
class ClockActivity : AppCompatActivity() {
private val viewModel: ClockViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_clock)
// 观察时间
viewModel.currentTime.observe(this) { time ->
timeTextView.text = time
}
}
}
布局 activity_clock.xml:
运行后,界面每秒更新当前时间(格式如“14:35:22”)。当Activity暂停(比如屏幕关闭),定时器自动停止;恢复时重新启动。零内存泄漏,超省资源!
小技巧:可以用协程替代 Timer,更现代:
override fun onActive() {
viewModelScope.launch {
while (isActive) {
postValue(formatter.format(Date()))
delay(1000)
}
}
}
假设我们要监听设备的加速度传感器数据,创建一个 SensorLiveData:
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.lifecycle.LiveData
data class Acceleration(val x: Float, val y: Float, val z: Float)
class SensorLiveData(context: Context) : LiveData() {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
private val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
postValue(Acceleration(event.values[0], event.values[1], event.values[2]))
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
override fun onActive() {
sensorManager.registerListener(listener, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
}
override fun onInactive() {
sensorManager.unregisterListener(listener)
}
}
使用:
ViewModel:val acceleration = SensorLiveData(context)
Activity:观察 acceleration 并更新UI,比如显示设备倾斜角度。
在现代应用中,分页加载(比如列表下拉刷新、上拉加载更多)是标配。Android的 Paging 3 库与LiveData天生一对,帮你轻松实现丝滑的分页体验。
基于Part 3的Todo应用,咱们用Paging 3实现分页加载,假设每个页面加载10条Todo。
在 build.gradle 中:
dependencies {
implementation "androidx.paging:paging-runtime-ktx:3.3.2"
}
修改 TodoDao 支持分页查询:
@Dao
interface TodoDao {
@Query("SELECT * FROM todos ORDER BY id DESC")
fun getTodosPaged(): PagingSource
@Insert
suspend fun insert(todo: Todo)
}
更新 TodoRepository:
class TodoRepository(private val dao: TodoDao) {
fun getTodosPaged(): Flow> = Pager(
config = PagingConfig(pageSize = 10, enablePlaceholders = false),
pagingSourceFactory = { dao.getTodosPaged() }
).flow
suspend fun addTodo(title: String) {
dao.insert(Todo(title = title, isCompleted = false))
}
}
class TodoViewModel(private val repository: TodoRepository) : ViewModel() {
val todos: LiveData> = repository.getTodosPaged()
.cachedIn(viewModelScope)
.asLiveData()
private val _error = MutableLiveData()
val error: LiveData get() = _error
fun addTodo(title: String) {
viewModelScope.launch {
if (title.isBlank()) {
_error.value = "Title cannot be empty!"
} else {
repository.addTodo(title)
}
}
}
}
解析:
Pager 创建分页数据流,pageSize 设置每页10条。
cachedIn(viewModelScope) 缓存分页数据,屏幕旋转不重新加载。
asLiveData() 将Flow转为LiveData,供UI观察。
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.todo_fragment.*
import kotlinx.coroutines.flow.collectLatest
import androidx.lifecycle.lifecycleScope
class TodoFragment : Fragment(R.layout.todo_fragment) {
private val viewModel: TodoViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置RecyclerView
todoRecyclerView.layoutManager = LinearLayoutManager(context)
val adapter = TodoPagingAdapter()
todoRecyclerView.adapter = adapter
// 观察分页数据
viewLifecycleOwner.lifecycleScope.launch {
viewModel.todos.observe(viewLifecycleOwner) { pagingData ->
adapter.submitData(pagingData)
}
}
// 观察错误
viewModel.error.observe(viewLifecycleOwner) { error ->
errorTextView.text = error
}
// 添加Todo
addButton.setOnClickListener {
viewModel.addTodo(todoEditText.text.toString())
todoEditText.text.clear()
}
}
}
class TodoPagingAdapter : PagingDataAdapter(TodoDiffCallback()) {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val titleTextView: TextView = view.findViewById(android.R.id.text1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val todo = getItem(position) ?: return
holder.titleTextView.text = todo.title
}
}
注意:布局文件 todo_fragment.xml 与Part 3相同。
运行后,Todo列表按需加载,每页10条。滑动到底部自动加载下一页,新增Todo后列表自动刷新。Paging 3的LiveData
随着协程和Flow的流行,很多项目开始从LiveData迁移到Flow。以下是平滑迁移的步骤和注意事项。
LiveData代码:
class OldViewModel : ViewModel() {
private val _data = MutableLiveData()
val data: LiveData get() = _data
fun updateData(newValue: String) {
_data.value = newValue
}
}
迁移到StateFlow:
class NewViewModel : ViewModel() {
private val _data = MutableStateFlow("")
val data: StateFlow get() = _data.asStateFlow()
fun updateData(newValue: String) {
_data.value = newValue
}
}
Activity观察:
viewModel.data.observe(this) { value ->
textView.text = value
}
改为:
lifecycleScope.launch {
viewModel.data.collect { value ->
textView.text = value
}
}
注意:
StateFlow 需要用 collect 在协程中观察。
用 lifecycleScope 确保生命周期安全。
LiveData的粘性事件在Flow中需要用 StateFlow 的初始值模拟:
private val _data = MutableStateFlow("Initial Value")
如果不需要粘性事件,用 SharedFlow:
private val _event = MutableSharedFlow()
val event = _event.asSharedFlow()
LiveData自动处理生命周期,Flow需要手动绑定:
lifecycleScope.launchWhenStarted {
viewModel.data.collect { value ->
textView.text = value
}
}
如果项目中有大量LiveData代码,可以用 asFlow() 过渡:
viewModel.liveData.asFlow().collect { value ->
textView.text = value
}
反之,Flow转LiveData:
flow.asLiveData()
测试:Flow的测试需要 kotlinx-coroutines-test,确保用 runBlockingTest。
性能:StateFlow 的 collect 可能触发多次,用 distinctUntilChanged() 优化。
兼容性:老项目保留LiveData,新模块用Flow,逐步迁移。
小贴士:迁移时先从ViewModel入手,UI层最后改,减少一次性改动风险。
LiveData用得好,能让应用丝滑;用不好,可能导致内存泄漏或性能瓶颈。以下是几个实用技巧。
工具:用LeakCanary检测LiveData相关的泄漏:
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14"
常见问题:用 observeForever 忘记 removeObserver。
解决:优先用 observe(lifecycleOwner),自动管理生命周期。
日志:重写 LiveData 的 setValue 打印更新:
class DebugLiveData : MutableLiveData() {
override fun setValue(value: T) {
Log.d("DebugLiveData", "New value: $value")
super.setValue(value)
}
}
Profiler:用Android Studio的Profiler监控LiveData更新频率。
用 distinctUntilChanged 过滤重复数据。
合并多次更新:
val batchLiveData = MediatorLiveData>().apply {
var tempList = mutableListOf()
addSource(source1) { value ->
tempList.add(value)
if (tempList.size >= BATCH_SIZE) {
this.value = tempList
tempList = mutableListOf()
}
}
}
在 ViewModel.onCleared() 中释放LiveData相关资源:
override fun onCleared() {
// 清理定时器、传感器等
super.onCleared()
}
在实际开发中,数据往往来自多个源头,比如本地数据库、服务器API,甚至实时WebSocket。如何让这些数据“和谐共舞”,实时同步到UI?LiveData的 MediatorLiveData 和自定义逻辑能帮你完美解决这个问题!
假设我们开发一个聊天应用,消息列表需要同步以下数据源:
本地数据库:缓存历史消息,供离线查看。
网络API:获取最新消息。
WebSocket:接收实时消息。
咱们用LiveData实现一个消息列表,自动合并这三者的数据。
data class Message(
val id: Long,
val content: String,
val sender: String,
val timestamp: Long
)
MessageDao.kt:
@Dao
interface MessageDao {
@Query("SELECT * FROM messages ORDER BY timestamp DESC")
fun getMessages(): LiveData>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMessages(messages: List)
}
MessageApi.kt:
interface MessageApi {
suspend fun fetchMessages(since: Long): List
}
WebSocketService.kt(模拟实时消息):
class WebSocketService {
private val _messages = MutableLiveData()
val messages: LiveData get() = _messages
// 模拟收到新消息
fun simulateNewMessage() {
viewModelScope.launch {
delay(2000)
_messages.postValue(Message(
id = System.currentTimeMillis(),
content = "New message!",
sender = "Server",
timestamp = System.currentTimeMillis()
))
}
}
}
MessageRepository.kt:
class MessageRepository(
private val dao: MessageDao,
private val api: MessageApi,
private val webSocket: WebSocketService
) {
private val _messages = MediatorLiveData>()
val messages: LiveData> get() = _messages
init {
// 监听本地数据库
_messages.addSource(dao.getMessages()) { localMessages ->
combineMessages(localMessages, _messages.value)
}
// 监听WebSocket
_messages.addSource(webSocket.messages) { newMessage ->
viewModelScope.launch {
dao.insertMessages(listOf(newMessage))
}
}
// 初始加载网络数据
fetchNetworkMessages()
}
private fun combineMessages(local: List?, current: List?) {
_messages.value = local ?: current ?: emptyList()
}
fun fetchNetworkMessages() {
viewModelScope.launch {
try {
val lastTimestamp = _messages.value?.maxOfOrNull { it.timestamp } ?: 0
val newMessages = api.fetchMessages(since = lastTimestamp)
dao.insertMessages(newMessages)
} catch (e: Exception) {
// 错误处理留给ViewModel
}
}
}
}
解析:
MediatorLiveData 合并本地数据库和WebSocket的数据。
本地数据库优先,网络数据异步补充,WebSocket实时插入。
新消息通过数据库触发UI更新,确保一致性。
class ChatViewModel(private val repository: MessageRepository) : ViewModel() {
val messages: LiveData> = repository.messages
private val _error = MutableLiveData()
val error: LiveData get() = _error
fun refreshMessages() {
repository.fetchNetworkMessages()
}
}
ChatFragment.kt:
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.chat_fragment.*
class ChatFragment : Fragment(R.layout.chat_fragment) {
private val viewModel: ChatViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置RecyclerView
messageRecyclerView.layoutManager = LinearLayoutManager(context)
val adapter = MessageAdapter()
messageRecyclerView.adapter = adapter
// 观察消息
viewModel.messages.observe(viewLifecycleOwner) { messages ->
adapter.submitList(messages)
}
// 观察错误
viewModel.error.observe(viewLifecycleOwner) { error ->
errorTextView.text = error
}
// 刷新
refreshButton.setOnClickListener {
viewModel.refreshMessages()
}
}
}
class MessageAdapter : ListAdapter(MessageDiffCallback()) {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val contentTextView: TextView = view.findViewById(android.R.id.text1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val message = getItem(position)
holder.contentTextView.text = "${message.sender}: ${message.content}"
}
}
class MessageDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean = oldItem == newItem
}
chat_fragment.xml:
运行后,消息列表显示本地缓存的消息,点击“Refresh”从网络拉取最新消息,WebSocket每2秒模拟一条新消息,列表实时更新。多数据源无缝同步,体验丝滑!
进阶挑战:给消息列表加个“已读/未读”状态,用MediatorLiveData合并用户阅读状态和消息数据。试试看!
在低端设备上(比如2GB内存的老手机),LiveData的频繁更新可能导致卡顿或OOM。以下是针对低端设备的优化技巧。
问题:每个Fragment创建多个LiveData,内存占用高。
解决:在Activity或共享ViewModel中统一管理LiveData:
class SharedViewModel : ViewModel() {
val sharedData: LiveData = MutableLiveData()
}
用lazy延迟初始化LiveData:
val heavyData: LiveData> by lazy {
repository.getHeavyData()
}
用MediatorLiveData合并多次小更新为一次大更新:
val batchedData = MediatorLiveData>().apply {
val tempList = mutableListOf()
addSource(source) { value ->
tempList.add(value)
if (tempList.size >= 10) {
this.value = tempList.toList()
tempList.clear()
}
}
}
对于实时数据(比如传感器),用采样降低频率:
class SensorLiveData(context: Context) : LiveData() {
private var lastUpdate = 0L
private val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
val now = System.currentTimeMillis()
if (now - lastUpdate >= 500) { // 每0.5秒更新
postValue(Acceleration(event.values[0], event.values[1], event.values[2]))
lastUpdate = now
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
}
用LeakCanary监控LiveData泄漏。
避免在observeForever后忘记移除:
val observer = Observer { /* ... */ }
liveData.observeForever(observer)
// 清理
liveData.removeObserver(observer)
小贴士:在低端设备上,优先用Room的LiveData查询,避免手动管理大数据集。
LiveData在开源社区被广泛使用,以下是从热门项目中提炼的最佳实践。
项目:android-architecture-components(GitHub)
实践:用LiveData结合Room和ViewModel,实现单一数据源(Single Source of Truth)。
启发:优先用数据库作为核心数据源,网络数据通过Repository同步。
项目:workflow-kotlin(GitHub)
实践:用LiveData包装状态机,UI只观察最终状态。
启发:复杂状态用MediatorLiveData合并,简化UI逻辑。
趋势:Compose中用LiveData.asState()观察:
@Composable
fun MyScreen(viewModel: MyViewModel) {
val data by viewModel.data.asState()
Text(text = data)
}
社区做法:封装LiveData为密封类状态:
sealed class State {
data class Success(val data: T) : State()
data class Error(val message: String) : State()
class Loading : State()
}
class StateViewModel : ViewModel() {
private val _state = MutableLiveData>()
val state: LiveData> get() = _state
}
优点:UI层只需处理三种状态,代码更清晰。
彩蛋:逛GitHub时,搜索“LiveData MVVM”能找到一堆宝藏项目,推荐看看Tivi和NowInAndroid!
最后,咱们来个大招:一个简化的社交应用,包含用户动态列表、点赞功能和评论实时更新,用LiveData串联所有功能。
app/
├── data/
│ ├── db/
│ │ ├── PostDao.kt
│ │ ├── AppDatabase.kt
│ ├── network/
│ │ ├── PostApi.kt
│ ├── repository/
│ │ ├── PostRepository.kt
├── ui/
│ ├── feed/
│ │ ├── FeedViewModel.kt
│ │ ├── FeedFragment.kt
│ │ ├── feed_fragment.xml
Post.kt:
@Entity(tableName = "posts")
data class Post(
@PrimaryKey val id: Long,
val user: String,
val content: String,
val likes: Int,
val comments: List,
val timestamp: Long
)
PostDao.kt:
@Dao
interface PostDao {
@Query("SELECT * FROM posts ORDER BY timestamp DESC")
fun getPosts(): LiveData>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertPosts(posts: List)
}
PostApi.kt:
interface PostApi {
suspend fun fetchPosts(since: Long): List
suspend fun likePost(postId: Long): Post
suspend fun addComment(postId: Long, comment: String): Post
}
PostRepository.kt:
class PostRepository(
private val dao: PostDao,
private val api: PostApi
) {
val posts: LiveData> = dao.getPosts()
private val _events = MutableLiveData>()
val events: LiveData> get() = _events
init {
fetchPosts()
}
fun fetchPosts() {
viewModelScope.launch {
try {
val lastTimestamp = posts.value?.maxOfOrNull { it.timestamp } ?: 0
val newPosts = api.fetchPosts(since = lastTimestamp)
dao.insertPosts(newPosts)
} catch (e: Exception) {
_events.value = Event("Failed to fetch posts: ${e.message}")
}
}
}
fun likePost(postId: Long) {
viewModelScope.launch {
try {
val updatedPost = api.likePost(postId)
dao.insertPosts(listOf(updatedPost))
} catch (e: Exception) {
_events.value = Event("Failed to like post: ${e.message}")
}
}
}
fun addComment(postId: Long, comment: String) {
viewModelScope.launch {
try {
val updatedPost = api.addComment(postId, comment)
dao.insertPosts(listOf(updatedPost))
} catch (e: Exception) {
_events.value = Event("Failed to comment: ${e.message}")
}
}
}
}
Event.kt(避免粘性事件):
class Event(private val content: T) {
private var hasBeenHandled = false
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
}
FeedViewModel.kt:
class FeedViewModel(private val repository: PostRepository) : ViewModel() {
val posts: LiveData> = repository.posts
val events: LiveData> = repository.events
fun refreshPosts() {
repository.fetchPosts()
}
fun likePost(postId: Long) {
repository.likePost(postId)
}
fun addComment(postId: Long, comment: String) {
repository.addComment(postId, comment)
}
}
FeedFragment.kt:
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.feed_fragment.*
class FeedFragment : Fragment(R.layout.feed_fragment) {
private val viewModel: FeedViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置RecyclerView
feedRecyclerView.layoutManager = LinearLayoutManager(context)
val adapter = PostAdapter(
onLikeClick = { postId -> viewModel.likePost(postId) },
onCommentClick = { postId, comment -> viewModel.addComment(postId, comment) }
)
feedRecyclerView.adapter = adapter
// 观察动态
viewModel.posts.observe(viewLifecycleOwner) { posts ->
adapter.submitList(posts)
}
// 观察事件
viewModel.events.observe(viewLifecycleOwner) { event ->
event.getContentIfNotHandled()?.let { message ->
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
// 刷新
refreshButton.setOnClickListener {
viewModel.refreshPosts()
}
}
}
class PostAdapter(
private val onLikeClick: (Long) -> Unit,
private val onCommentClick: (Long, String) -> Unit
) : ListAdapter(PostDiffCallback()) {
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val contentTextView: TextView = view.findViewById(R.id.contentTextView)
val likesTextView: TextView = view.findViewById(R.id.likesTextView)
val commentsTextView: TextView = view.findViewById(R.id.commentsTextView)
val likeButton: Button = view.findViewById(R.id.likeButton)
val commentEditText: EditText = view.findViewById(R.id.commentEditText)
val commentButton: Button = view.findViewById(R.id.commentButton)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_post, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val post = getItem(position)
holder.contentTextView.text = "${post.user}: ${post.content}"
holder.likesTextView.text = "Likes: ${post.likes}"
holder.commentsTextView.text = "Comments: ${post.comments.joinToString("\n")}"
holder.likeButton.setOnClickListener { onLikeClick(post.id) }
holder.commentButton.setOnClickListener {
val comment = holder.commentEditText.text.toString()
if (comment.isNotBlank()) {
onCommentClick(post.id, comment)
holder.commentEditText.text.clear()
}
}
}
}
class PostDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean = oldItem == newItem
}
item_post.xml:
feed_fragment.xml:
运行后,用户看到动态列表,可以点赞或评论。点击“Refresh”拉取最新动态,错误通过Toast提示。LiveData确保数据实时同步,UI响应丝滑,完美诠释了MVVM的优雅!
终极挑战:给动态列表加分页加载(参考Part 4),并实现“已读”标记功能。试试看,成果可以分享给我!