You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ViewModel、LiveData和RxJava正确处理RecyclerView数据与加载指示器

解决后台恢复后LiveData仅收到加载结束通知的问题

这个问题我之前也碰到过,核心原因就是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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:03:05