Android 之 Jetpack - Paging

以下是一个基于 Android Paging 3 库的完整 Kotlin 实现示例,结合网络分页场景,包含核心组件和最佳实践:

1. ​​添加依赖(build.gradle)​

dependencies {
    implementation "androidx.paging:paging-runtime-ktx:3.2.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
    implementation "com.squareup.retrofit2:retrofit:2.9.0" // 网络请求
}

2. ​​定义数据模型

data class Article(
    val id: Long,
    val title: String,
    val content: String
)

3. ​​实现 PagingSource(网络数据源)​

class ArticlePagingSource(
    private val apiService: ApiService
) : PagingSource<Int, Article>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
        return try {
            val page = params.key ?: 1 // 起始页码
            val response = apiService.getArticles(page, params.loadSize)
            
            LoadResult.Page(
                data = response.articles,
                prevKey = if (page > 1) page - 1 else null, // 上一页
                nextKey = if (response.hasMore) page + 1 else null // 下一页
            )
        } catch (e: Exception) {
            LoadResult.Error(e) // 自动传递错误到UI 
        }
    }

    override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
        // 根据滚动位置计算刷新后的起始页码 
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

4. ​​在 ViewModel 中配置数据流

class ArticleViewModel : ViewModel() {
    private val api = RetrofitClient.create<ApiService>()
    
    val articles: Flow<PagingData<Article>> = Pager(
        config = PagingConfig(
            pageSize = 20,          // 每页数据量
            prefetchDistance = 5,   // 提前加载下一页的阈值(item数量)
            enablePlaceholders = false // 禁用占位符提升性能 
        ),
        pagingSourceFactory = { ArticlePagingSource(api) }
    ).flow
      .cachedIn(viewModelScope) // 防止重复创建数据流 
}

5. ​​实现 PagingDataAdapter​

class ArticleAdapter : PagingDataAdapter<Article, ArticleAdapter.ViewHolder>(DIFF_CALLBACK) {

    companion object {
        private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Article>() {
            override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem.id == newItem.id // ID 相同视为同一项[5](@ref)
            
            override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean =
                oldItem == newItem // 内容完全一致
        }
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        fun bind(article: Article) {
            itemView.findViewById<TextView>(R.id.title).text = article.title
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_article, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        getItem(position)?.let { holder.bind(it) }
    }
}

6. ​​在 Activity/Fragment 中集成

class ArticleListFragment : Fragment() {
    private lateinit var binding: FragmentArticleListBinding
    private val viewModel: ArticleViewModel by viewModels()
    private val adapter = ArticleAdapter()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(requireContext())
            adapter = this@ArticleListFragment.adapter
        }

        // 监听加载状态(加载中/错误/完成)
        adapter.addLoadStateListener { loadState ->
            when (loadState.refresh) {
                is LoadState.Loading -> showProgressBar()
                is LoadState.Error -> showError("加载失败")
                is LoadState.NotLoading -> hideProgressBar()
            }
        }

        // 绑定数据流
        lifecycleScope.launch {
            viewModel.articles.collectLatest { pagingData ->
                adapter.submitData(pagingData) // 提交分页数据 
            }
        }
    }

    // 错误重试逻辑
    private fun showError(message: String) {
        binding.errorText.text = message
        binding.retryButton.setOnClickListener { adapter.retry() } // 自动重试失败页 
    }
}

关键配置说明​

  1. ​PagingConfig 参数优化​

    • prefetchDistance:建议设为屏幕可见项数量的 1-2 倍(如 5-10)

    • maxSize:设置缓存上限(默认 200),防止内存溢出

    • enablePlaceholders:关闭占位符可减少内存占用

  2. ​错误处理机制​

    • adapter.retry():自动重试失败的分页请求

    • LoadResult.Error:在 UI 层通过 LoadState监听错误

  3. ​性能优化​

    • cachedIn(viewModelScope)​:避免重复创建数据流

    • ​高效 DiffUtil​​:通过 areItemsTheSame快速比较数据变更

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值