使用ViewModel、LiveData和RxJava正确处理RecyclerView数据与加载指示器
这个问题我之前也碰到过,核心原因就是LiveData的特性:它只会给处于活跃状态的观察者发送数据,当应用在后台时观察者是非活跃的,此时发送的success(results)和loading(false)会被LiveData缓存,但LiveData只会保留最后一次的value。所以当你恢复应用后,观察者只能收到最后一次的loading(false)通知,之前的成功数据就被覆盖了,导致RecyclerView没数据。
咱们先看你的代码问题:你把加载状态和数据状态拆成了独立的LiveData事件发送,但LiveData不是事件总线,它是用来保存和观察状态的,这种拆分发送的方式就会导致状态丢失。
下面给你两种可行的解决方案,推荐第一种,更符合状态管理的思路:
方案一:让Outcome状态包含完整的必要信息
调整你的Outcome类,让每个状态都能携带当前的缓存数据(如果有的话),这样即使最后一次发送的是加载结束状态,也能带上之前加载好的数据,UI层就能正确渲染了。
1. 修改Outcome密封类
sealed class Outcome<T> { // 加载状态:携带缓存数据,避免切换时丢失 data class Loading<T>(val isLoading: Boolean, val cachedData: T? = null) : Outcome<T>() data class Success<T>(val data: T) : Outcome<T>() data class Failure<T>(val e: Throwable, val cachedData: T? = null) : Outcome<T>() companion object { fun <T> loading(isLoading: Boolean, cachedData: T? = null): Outcome<T> = Loading(isLoading, cachedData) fun <T> success(data: T): Outcome<T> = Success(data) fun <T> failure(e: Throwable, cachedData: T? = null): Outcome<T> = Failure(e, cachedData) } }
2. 调整SearchViewModel逻辑
在ViewModel中保存当前已加载的数据,发送加载状态时带上这个缓存,确保状态的连续性:
class SearchViewModel @Inject constructor( private val dataSource: MusicInfoRepositoryInterface, private val scheduler: Scheduler, private val disposables: CompositeDisposable ) : ViewModel() { private val searchOutcome = MutableLiveData<Outcome<List<MusicInfo>>>() val searchOutcomLiveData: LiveData<Outcome<List<MusicInfo>>> get() = searchOutcome // 保存当前已加载的数据,用于状态切换时携带 private var currentData: List<MusicInfo>? = null fun search(searchText: String) { Timber.d(".loadMusicInfos") if(searchText.isBlank()) { return } dataSource.search(searchText) .observeOn(scheduler.mainThread()) // 开始加载时,携带当前已有的缓存数据 .startWith(Outcome.loading(true, currentData)) .map { result -> // 保存成功加载的数据 currentData = result Outcome.success(result) } .onErrorReturn { throwable -> // 错误时也携带缓存数据,避免UI空掉 Outcome.failure(throwable, currentData) } .doOnTerminate { // 加载结束时,发送携带缓存数据的加载状态 searchOutcome.value = Outcome.loading(false, currentData) } .subscribeWith(object : DisposableSubscriber<Outcome<List<MusicInfo>>>() { override fun onNext(outcome: Outcome<List<MusicInfo>>?) { searchOutcome.value = outcome } override fun onError(e: Throwable) { Timber.d(e, ".onError") } override fun onComplete() { Timber.d(".onComplete") } }).addTo(disposables) } override fun onCleared() { Timber.d(".onCleared") super.onCleared() disposables.clear() } }
3. UI层处理逻辑
在Activity/Fragment的观察者中,根据不同状态处理,同时利用缓存数据:
viewModel.searchOutcomLiveData.observe(this) { outcome -> when(outcome) { is Outcome.Loading -> { // 控制进度条显示/隐藏 progressBar.visibility = if(outcome.isLoading) View.VISIBLE else View.GONE // 如果有缓存数据,立即更新RecyclerView outcome.cachedData?.let { musicList -> updateRecyclerView(musicList) } } is Outcome.Success -> { progressBar.visibility = View.GONE updateRecyclerView(outcome.data) } is Outcome.Failure -> { progressBar.visibility = View.GONE // 显示错误提示,同时用缓存数据保持UI不空白 outcome.cachedData?.let { updateRecyclerView(it) } Toast.makeText(this, outcome.e.message, Toast.LENGTH_SHORT).show() } } }
方案二:用Event包装类处理一次性事件
如果你不想修改Outcome的结构,可以用一个Event类来包装每个LiveData事件,确保每个事件都能被观察者处理,而不是只保留最新值。这种方式更适合一次性UI事件(比如Toast、导航),但也能解决你的问题:
1. 定义Event类
open class Event<out T>(private val content: T) { var hasBeenHandled = false private set // 只允许外部读取,不允许修改 /** * 获取内容并标记为已处理,避免重复处理 */ fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } /** * 获取内容,即使已经处理过(用于需要重复读取的场景) */ fun peekContent(): T = content }
2. 修改ViewModel的LiveData类型
private val searchOutcome = MutableLiveData<Event<Outcome<List<MusicInfo>>>>() val searchOutcomLiveData: LiveData<Event<Outcome<List<MusicInfo>>>> get() = searchOutcome // 发送事件时用Event包装 searchOutcome.value = Event(Outcome.success(results)) searchOutcome.value = Event(Outcome.loading(false))
3. UI层观察处理
viewModel.searchOutcomLiveData.observe(this) { event -> event.getContentIfNotHandled()?.let { outcome -> when(outcome) { is Outcome.Progress -> { progressBar.visibility = if(outcome.loading) View.VISIBLE else View.GONE } is Outcome.Success -> { updateRecyclerView(outcome.data) } is Outcome.Failure -> { Toast.makeText(this, outcome.e.message, Toast.LENGTH_SHORT).show() } } } }
总结
推荐用方案一,因为它更符合LiveData的状态管理定位——UI可以根据当前的Outcome状态完全恢复,而不是依赖事件顺序。方案二更适合处理一次性的UI触发事件,不是最佳的状态管理方式。
内容的提问来源于stack exchange,提问作者Mark Pazon

