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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 19:48:02