如何处理基于LiveData的Kotlin Jetpack Paging 3异常?
实现方案(仅使用LiveData,无需Flow)
1 首先修正现有代码的问题
你当前代码有两处可优化点:
- ViewModel 中
getAllGitRepos用 suspend 修饰没有必要,Pager.liveData本身是异步回调的冷LiveData,直接暴露即可,不需要挂起 - Activity 中把
observe放到lifecycleScope.launch里是多余操作,observe本身就是生命周期感知的异步回调
2 核心实现逻辑
Paging的加载异常会封装在 LoadState 中,我们通过PagingAdapter的状态回调捕获异常,再通过ViewModel的LiveData透传到UI层展示即可。
2.1 可选优化:Resource类改成密封类(更安全)
原来的data class+枚举的写法容易漏状态,改成密封类后编译器会强制检查所有分支:
sealed class Resource<out T> { object Loading : Resource<Nothing>() data class Success<out T>(val data: T) : Resource<T>() data class Error(val message: String, val throwable: Throwable? = null) : Resource<Nothing>() }
如果要保留你原来的Resource类也可以正常使用。
2.2 新增一次性事件包装类(避免重复触发错误)
解决LiveData粘性事件导致旋转屏幕后重复弹出错误的问题:
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 } } }
2.3 修改GitRepoPagingSource捕获异常
class GitRepoPagingSource(private val service: GitRepoApi) : PagingSource<Int, GitRepo>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GitRepo> { return try { val currentPage = params.key ?: 1 val response = service.getRepos(currentPage, params.loadSize) LoadResult.Page( data = response.items, prevKey = if (currentPage == 1) null else currentPage - 1, nextKey = if (response.items.isEmpty()) null else currentPage + 1 ) } catch (e: Exception) { // 异常返回给Paging框架,后续会透传到LoadState中 LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, GitRepo>): Int? { return state.anchorPosition?.let { anchor -> state.closestPageToPosition(anchor)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchor)?.nextKey?.minus(1) } } }
2.4 修改ViewModel暴露状态
class GitRepoViewModel(private val repository: GitRepoRepository) : ViewModel() { // 分页数据直接暴露,自动缓存 val gitReposList = repository.getListData().cachedIn(viewModelScope) // 错误事件,一次性消费 private val _errorMsg = MutableLiveData<Event<String>>() val errorMsg: LiveData<Event<String>> = _errorMsg // 加载状态 private val _loadState = MutableLiveData<Resource<Unit>>() val loadState: LiveData<Resource<Unit>> = _loadState // 供UI层调用,上报加载错误 fun onLoadError(throwable: Throwable) { val errorMsg = throwable.localizedMessage ?: "未知加载错误" _loadState.postValue(Resource.Error(errorMsg, throwable)) _errorMsg.postValue(Event(errorMsg)) } // 供UI层调用,上报加载状态变化 fun onLoading() { _loadState.postValue(Resource.Loading) } fun onLoadSuccess() { _loadState.postValue(Resource.Success(Unit)) } }
2.5 修改Activity实现异常展示
class PagingActivity : AppCompatActivity() { private lateinit var gitRepoViewModel: GitRepoViewModel private lateinit var recyclerViewAdapter: GitRepoAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 省略初始化逻辑 // 观察分页数据提交给Adapter gitRepoViewModel.gitReposList.observe(this) { pagingData -> recyclerViewAdapter.submitData(lifecycle, pagingData) } // 监听Adapter加载状态(纯回调,无需Flow) recyclerViewAdapter.addLoadStateListener { loadStates -> // 处理下拉刷新状态 when (val refreshState = loadStates.refresh) { is LoadState.Loading -> gitRepoViewModel.onLoading() is LoadState.Error -> gitRepoViewModel.onLoadError(refreshState.error) is LoadState.NotLoading -> gitRepoViewModel.onLoadSuccess() } // 处理上拉加载更多的异常 val appendState = loadStates.append if (appendState is LoadState.Error) { gitRepoViewModel.onLoadError(appendState.error) } } // 观察错误事件,展示到TextView gitRepoViewModel.errorMsg.observe(this) { event -> event.getContentIfNotHandled()?.let { msg -> // 直接赋值到你的错误提示TextView errorTextView.text = msg // 可选:3秒后自动隐藏错误提示 errorTextView.postDelayed({ errorTextView.visibility = View.GONE }, 3000) } } // 观察加载状态,控制加载弹窗显隐 gitRepoViewModel.loadState.observe(this) { resource -> when(resource) { is Resource.Loading -> loadingProgress.visibility = View.VISIBLE is Resource.Success, is Resource.Error -> loadingProgress.visibility = View.GONE } } } }
3 注意事项
- 不需要把PagingData包装到Resource里,PagingData本身是流式分页数据,和加载/错误状态分开管理更合理
- 所有状态都经过ViewModel处理,配置变更(比如旋转屏幕)不会丢失状态,也不会重复触发错误提示
- 如果你不需要在ViewModel层处理异常,也可以直接在Activity的addLoadStateListener回调里直接把错误赋值到TextView,代码更简洁
内容的提问来源于stack exchange,提问作者Roy Hayek
相关产品推荐
相关产品推荐

