Kotlin:Retrofit+Paging3无网络时错误布局不显示问题
问题:Paging 3无网络时错误布局不显示(日志显示hasError为true但布局未生效)
问题详情
开发Android应用,通过Retrofit调用TMDB API获取电影数据,使用Paging 3 + Flow实现分页加载。加载进度条显示/隐藏正常,但无网络时错误布局(重试按钮)始终无法显示。已通过ViewModel的hasError LiveData绑定布局可见性,日志输出hasError值为true,但布局可见性未发生变化。
相关代码
MoviesViewModel.kt
class MoviesViewModel: ViewModel() { val isLoading: MutableLiveData<Boolean> = MutableLiveData() val hasError: MutableLiveData<Boolean> = MutableLiveData() fun getData(): Flow<PagingData<MoviesModel>> { return MoviesRepository.getData() } }
MoviesRepository.kt
object MoviesRepository { fun getData(): Flow<PagingData<MoviesModel>> { return Pager( config = PagingConfig( pageSize = Constants.QUERY_PAGE_SIZE, enablePlaceholders = false ), pagingSourceFactory = { MoviesPagingSource(RetrofitInstance.api) } ).flow } }
MoviesPagingSource.kt
class MoviesPagingSource( private val service: ApiService ) : PagingSource<Int, MoviesModel>() { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MoviesModel> { val position = params.key ?: FIRST_PAGE return try { val response = service.getMovies(page = position) val movies = response.body()?.results val nextKey = if (movies!!.isEmpty()) { null } else { position + (params.loadSize / QUERY_PAGE_SIZE) } LoadResult.Page( data = movies, prevKey = if (position == FIRST_PAGE) null else position, nextKey = nextKey ) } catch (e: IOException) { LoadResult.Error(e) } catch (e: HttpException) { LoadResult.Error(e) } } override fun getRefreshKey(state: PagingState<Int, MoviesModel>): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } }
MoviesPage.kt
private lateinit var pagerAdapter: MoviesAdapter ... override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewLifecycleOwner.lifecycleScope.launch { pagerAdapter.loadStateFlow.collectLatest { loadStates -> viewModel.isLoading.value = loadStates.refresh is LoadState.Loading // 设置进度条可见性 viewModel.hasError.value = loadStates.refresh is LoadState.Error // 显示错误信息 Log.d("ERROR", viewModel.hasError.value.toString()) // 返回true } } lifecycleScope.launch { viewModel.getData().collectLatest { pagerAdapter.submitData(it) } } } private fun setupRecyclerView() { // 在onCreateView中调用 pagerAdapter = MoviesAdapter() binding.movieRecycler.apply { adapter = pagerAdapter addItemDecoration(MarginDecoration(context)) } }
XML布局相关代码
进度条可见性:
android:visibility="@{viewModel.isLoading? View.VISIBLE: View.GONE}"
错误按钮可见性:
<Button android:id="@+id/retry" android:layout_width="match_parent" android:layout_height="50dp" android:text="Error" android:visibility="@{viewModel.hasError? View.VISIBLE: View.GONE}" />
MoviesAdapter.kt
class MoviesAdapter : PagingDataAdapter<MoviesModel, MoviesAdapter.MovieItemViewHolder>( differCallback ) { var onItemClick: ((MoviesModel) -> Unit)? = null inner class MovieItemViewHolder(private var binding: ItemMovieBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(movie: MoviesModel?) { binding.movieItem = movie binding.executePendingBindings() } } companion object { private val differCallback = object : DiffUtil.ItemCallback<MoviesModel>() { override fun areItemsTheSame(oldItem: MoviesModel, newItem: MoviesModel): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: MoviesModel, newItem: MoviesModel): Boolean { return oldItem == newItem } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieItemViewHolder { return MovieItemViewHolder( ItemMovieBinding.inflate(LayoutInflater.from(parent.context)) ) } override fun onBindViewHolder(holder: MovieItemViewHolder, position: Int) { holder.bind(getItem(position)) holder.itemView.setOnClickListener { onItemClick?.invoke(getItem(position)!!) } } }
解决方案
1. 调整布局层级,避免错误布局被遮挡
错误布局可能被RecyclerView遮挡,需确保错误按钮在布局层级中处于上层,同时在错误时隐藏RecyclerView:
<!-- 使用FrameLayout作为父容器 --> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/movieRecycler" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="@{viewModel.hasError ? View.GONE : View.VISIBLE}"/> <Button android:id="@+id/retry" android:layout_width="match_parent" android:layout_height="50dp" android:text="加载失败,点击重试" android:visibility="@{viewModel.hasError ? View.VISIBLE : View.GONE}"/> <ProgressBar android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"/> </FrameLayout>
2. 确保XML数据绑定正确导入View类
在布局的data标签中添加android.view.View的导入,否则View.VISIBLE/View.GONE无法被识别:
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="viewModel" type="com.yourpackage.MoviesViewModel"/> <!-- 必须导入View类 --> <import type="android.view.View"/> </data> <!-- 布局内容 --> </layout>
3. 使用postValue更新LiveData(避免线程问题)
在协程中更新LiveData时,使用postValue替代setValue,确保操作在主线程执行:
viewLifecycleOwner.lifecycleScope.launch { pagerAdapter.loadStateFlow.collectLatest { loadStates -> viewModel.isLoading.postValue(loadStates.refresh is LoadState.Loading) viewModel.hasError.postValue(loadStates.refresh is LoadState.Error) Log.d("ERROR", viewModel.hasError.value.toString()) } }
4. 添加重试逻辑
为错误按钮添加点击事件,触发Paging数据刷新:
// 在MoviesPage.kt的onViewCreated中 binding.retry.setOnClickListener { pagerAdapter.refresh() }
5. 优化PagingSource错误捕获(可选)
增加通用异常捕获,确保所有错误都能被正确传递:
// MoviesPagingSource.kt的load方法中 catch (e: IOException) { LoadResult.Error(e) } catch (e: HttpException) { LoadResult.Error(e) } catch (e: Exception) { // 捕获其他未知异常 LoadResult.Error(e) }
内容的提问来源于stack exchange,提问作者t3nsa
相关产品推荐
相关产品推荐

