当前位置: 首页 > news >正文

Android 使用Paging3 实现列表分页加载、下拉刷新、错误重试、筛选功能

Android 使用Paging3 实现列表加载

Paging3是Android Jetpack组件库中的分页加载库,它可以帮助开发者轻松实现列表数据的分页加载功能。本文将逐步讲解如何使用Paging3库实现一个带有加载更多、下拉刷新、错误重试、筛选功能的列表页面。

最终效果如下

加载更多、错误重试选择筛选向
在这里插入图片描述在这里插入图片描述

1. 添加依赖

首先,在应用的build.gradle.kts文件中添加Paging3相关的依赖:

implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.paging:paging-runtime-ktx:3.3.6")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
//下拉刷新库
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

2. 实现最基础的Paging3

2.1 创建数据模型

// 列表项数据类
data class ExampleItem(val id: Int, val content: String, val type: FilterType = FilterType.ALL)

2.2 创建PagingSource

ExamplePagingSource负责数据加载逻辑

class ExamplePagingSource : PagingSource<Int, ExampleItem>() {override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ExampleItem> {return try {val page = params.key ?: 0val items = (1..20).map { ExampleItem(id = page * 20 + it,content = "Item ${page * 20 + it}")}LoadResult.Page(data = items,prevKey = if (page > 0) page - 1 else null,nextKey = page + 1)} catch (e: Exception) {LoadResult.Error(e)}}override fun getRefreshKey(state: PagingState<Int, ExampleItem>): Int? {return state.anchorPosition?.let { anchorPosition ->state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)}}
}

2.3 创建Repository

ExampleRepository封装了PagingSource的创建

class ExampleRepository {fun examplePagingSource() = ExamplePagingSource()
}

2.4 创建ViewModel

创建ViewModel负责管理Paging数据流和处理业务逻辑

class MainViewModel() : ViewModel() {private val repository: ExampleRepository = ExampleRepository()val pagingData: Flow<PagingData<ExampleItem>> =Pager(config = PagingConfig(pageSize = 20),pagingSourceFactory = { repository.examplePagingSource() }).flow.cachedIn(viewModelScope).flowOn(Dispatchers.IO)
}

2.5 创建列表适配器

// RecyclerView适配器
class ExampleAdapter : PagingDataAdapter<ExampleItem, ExampleViewHolder>(DIFF_CALLBACK) {override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =ExampleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_example, parent, false))override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {getItem(position)?.let { holder.bind(it) }}companion object {private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ExampleItem>() {override fun areItemsTheSame(oldItem: ExampleItem, newItem: ExampleItem) = oldItem.id == newItem.idoverride fun areContentsTheSame(oldItem: ExampleItem, newItem: ExampleItem) = oldItem == newItem}}
}// ViewHolder实现
class ExampleViewHolder(val view: View) : RecyclerView.ViewHolder(view) {fun bind(item: ExampleItem) {view.findViewById<TextView>(R.id.tvContent).text = item.content}
}

新建item布局item_example.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><TextViewandroid:id="@+id/tvContent"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginLeft="24dp"android:textSize="14sp" /></FrameLayout>

2.6 创建界面Activity

Activity负责组装所有组件,显示列表,并处理用户交互:

class MainActivity : AppCompatActivity() {private lateinit var viewModel: MainViewModelprivate val adapter = ExampleAdapter()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()setContentView(R.layout.activity_main)setupRecyclerView()setupViewModel()}private fun setupRecyclerView() {findViewById<RecyclerView>(R.id.recyclerView).apply {layoutManager = LinearLayoutManager(this@MainActivity)adapter = this@MainActivity.adapter}}private fun setupViewModel() {viewModel = ViewModelProvider(this)[MainViewModel::class.java]lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.pagingData.collect { pagingData ->adapter.submitData(pagingData)}}}}
}

2.7 效果如下

在这里插入图片描述

3. 实现带有加载更多、错误重试、筛选功能的列表页面

3.1 创建数据模型

// 列表项数据类
data class ExampleItem(val id: Int, val content: String, val type: FilterType = FilterType.ALL)// 筛选类型枚举
enum class FilterType {ALL,TYPE_A,TYPE_B,TYPE_C
}

3.2 创建PagingSource

ExamplePagingSource负责数据加载逻辑

class ExamplePagingSource(private val filterType: FilterType) : PagingSource<Int, ExampleItem>() {companion object {// 设置最大页数,模拟数据有限的情况private const val MAX_PAGE = 5private const val TAG = "ExamplePagingSource"}override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ExampleItem> {return try {//模拟网络请求耗时delay(2000)val pageIndex = params.key ?: 0val pageSize = params.loadSizeif (Random.nextBoolean()) {throw IllegalStateException("加载失败,点击重试")}// 模拟达到数据末尾的情况if (pageIndex >= MAX_PAGE) {return LoadResult.Page(data = emptyList(),prevKey = if (pageIndex > 0) pageIndex - 1 else null,nextKey = null  // nextKey为null表示没有更多数据)}// 创建基础数据val allItems = (1..pageSize).map {val itemId = pageIndex * pageSize + itval itemType = filterTypeExampleItem(id = itemId,content = if (itemId % 3 == 0) {"Item ${itemId} [${itemType.name}]" + "Item ${itemId}" + "Item ${itemId}" + "Item ${itemId}" + "Item ${itemId}" + "Item ${itemId}"} else {"Item> ${itemId} [${itemType.name}] ${Random.nextInt(0, 100000)}"},type = itemType)}// 判断是否是最后一页val isLastPage = pageIndex == MAX_PAGE - 1val nextKey = if (allItems.isNotEmpty() && !isLastPage) pageIndex + 1 else nullLog.i(TAG, "nextKey=$nextKey")LoadResult.Page(data = allItems,prevKey = if (pageIndex > 0) pageIndex - 1 else null,nextKey = nextKey)} catch (e: Exception) {Log.i(TAG, "Error loading data: ${e.message}")// 确保错误被正确传递LoadResult.Error(e)}}override fun getRefreshKey(state: PagingState<Int, ExampleItem>): Int? {return state.anchorPosition?.let { anchorPosition ->state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)}}
}

3.3 创建Repository

ExampleRepository封装PagingSource的创建

class ExampleRepository {fun examplePagingSource(filterType: FilterType = FilterType.ALL) =ExamplePagingSource(filterType)
}

3.4 创建ViewModel

创建ViewModel负责管理Paging数据流和处理业务逻辑:

class LoadMoreViewModel() : ViewModel() {private val repository: ExampleRepository = ExampleRepository()// 当前筛选类型private val _currentFilter = MutableStateFlow<FilterType>(FilterType.ALL)// 每次筛选条件变化时,重新创建Pagerval pagingData: Flow<PagingData<ExampleItem>> = _currentFilter.flatMapLatest { filterType ->Pager(config = PagingConfig(pageSize = 50, initialLoadSize = 50),pagingSourceFactory = { repository.examplePagingSource(filterType) }).flow}.cachedIn(viewModelScope).flowOn(Dispatchers.IO)// 更新筛选条件fun updateFilter(filterType: FilterType) {_currentFilter.value = filterType}
}

3.5 创建列表适配器

3.5.1 创建PagingDataAdapter,处理分页数据
// RecyclerView适配器
class ExampleAdapter : PagingDataAdapter<ExampleItem, ExampleViewHolder>(DIFF_CALLBACK) {override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExampleViewHolder {return ExampleViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_example, parent, false))}override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) {getItem(position)?.let {holder.bind(it)}}companion object {private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ExampleItem>() {override fun areItemsTheSame(oldItem: ExampleItem, newItem: ExampleItem) =oldItem.id == newItem.idoverride fun areContentsTheSame(oldItem: ExampleItem, newItem: ExampleItem) =oldItem == newItem}}
}// ViewHolder实现
class ExampleViewHolder(val view: View) : RecyclerView.ViewHolder(view) {fun bind(item: ExampleItem) {view.findViewById<TextView>(R.id.tvContent).text = item.content}
}
3.5.2 创建加载状态适配器,处理加载状态显示
class LoadStateAdapter(private val retry: () -> Unit = {}) : LoadStateAdapter<com.zeekr.myviewcursortest.loadmore.LoadStateAdapter.LoadStateViewHolder>() {companion object {private const val TAG = "LoadStateAdapter"}override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState) =LoadStateViewHolder(LoadingFooterBinding.inflate(LayoutInflater.from(parent.context),parent,false),retry)override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {Log.d(TAG, "当前加载状态: $loadState, endOfPaginationReached: ${(loadState as? LoadState.NotLoading)?.endOfPaginationReached}")Log.i(TAG,"loadState.endOfPaginationReached:${loadState.endOfPaginationReached}")// 根据不同状态设置UIwhen (loadState) {is LoadState.Loading -> {holder.binding.loadingProgress.visibility = View.VISIBLEholder.binding.loadingText.text = "加载中..."holder.binding.loadingText.isClickable = false}is LoadState.Error -> {holder.binding.loadingProgress.visibility = View.GONEholder.binding.loadingText.text = "加载失败,请重试"holder.binding.loadingText.isClickable = trueholder.binding.loadingText.setOnClickListener { retry() }}is LoadState.NotLoading -> {holder.binding.loadingProgress.visibility = View.GONEif (loadState.endOfPaginationReached) {holder.binding.loadingText.text = "没有更多数据了"holder.binding.loadingText.visibility = View.VISIBLE} else {holder.binding.loadingText.text = ""holder.binding.loadingText.visibility = View.GONE}holder.binding.loadingText.isClickable = falseholder.binding.loadingText.setOnClickListener(null)}}}// 关键:确保当没有更多数据时也显示footeroverride fun displayLoadStateAsItem(loadState: LoadState): Boolean {return loadState is LoadState.Loading || loadState is LoadState.Error || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached)}class LoadStateViewHolder(val binding: LoadingFooterBinding,private val retry: () -> Unit) : RecyclerView.ViewHolder(binding.root)
}

3.6 创建界面Activity

3.6.1 创建XML布局

activity_load_more.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".loadmore.LoadMoreActivity"><!-- 筛选器布局 --><HorizontalScrollViewandroid:id="@+id/filter_scroll_view"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="#F5F5F5"android:scrollbars="none"app:layout_constraintTop_toTopOf="parent"><com.google.android.material.chip.ChipGroupandroid:id="@+id/filter_chip_group"android:layout_width="wrap_content"android:layout_height="wrap_content"android:padding="8dp"app:singleSelection="true"><com.google.android.material.chip.Chipandroid:id="@+id/filter_all"style="@style/Widget.MaterialComponents.Chip.Choice"android:layout_width="wrap_content"android:layout_height="wrap_content"android:checked="true"android:text="全部" /><com.google.android.material.chip.Chipandroid:id="@+id/filter_type_a"style="@style/Widget.MaterialComponents.Chip.Choice"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="类型A" /><com.google.android.material.chip.Chipandroid:id="@+id/filter_type_b"style="@style/Widget.MaterialComponents.Chip.Choice"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="类型B" /><com.google.android.material.chip.Chipandroid:id="@+id/filter_type_c"style="@style/Widget.MaterialComponents.Chip.Choice"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="类型C" /></com.google.android.material.chip.ChipGroup></HorizontalScrollView><androidx.swiperefreshlayout.widget.SwipeRefreshLayoutandroid:id="@+id/swipeRefresh"android:layout_width="0dp"android:layout_height="0dp"android:visibility="gone"app:layout_constraintTop_toBottomOf="@id/filter_scroll_view"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="match_parent" /></androidx.swiperefreshlayout.widget.SwipeRefreshLayout><!-- 中央加载进度条 --><ProgressBarandroid:id="@+id/center_loading"android:layout_width="50dp"android:layout_height="50dp"app:layout_constraintTop_toBottomOf="@id/filter_scroll_view"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"/><!-- 错误重试布局 --><LinearLayoutandroid:id="@+id/error_view"android:layout_width="wrap_content"android:layout_height="wrap_content"android:orientation="vertical"android:gravity="center"android:visibility="gone"app:layout_constraintTop_toBottomOf="@id/filter_scroll_view"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"><ImageViewandroid:layout_width="60dp"android:layout_height="60dp"android:src="@android:drawable/ic_dialog_alert"android:contentDescription="错误图标"/><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="8dp"android:text="加载失败"/><Buttonandroid:id="@+id/btn_retry"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:text="重试"/></LinearLayout></androidx.constraintlayout.widget.ConstraintLayout>
3.6.2 创建LoadMoreActivity
class LoadMoreActivity : AppCompatActivity() {private lateinit var viewModel: LoadMoreViewModelprivate val adapter = ExampleAdapter()private val footerAdapter = LoadStateAdapter { adapter.retry() }private lateinit var binding: ActivityLoadMoreBindingcompanion object {private const val TAG = "LoadMoreActivity"}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)enableEdgeToEdge()// 初始化ViewBindingbinding = ActivityLoadMoreBinding.inflate(layoutInflater)setContentView(binding.root)ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)insets}setupFilterChips()setupSwipeRefresh()setupRecyclerView()setupViewModel()setupLoadStateListener()setupRetryButton()}private fun setupFilterChips() {// 设置筛选器点击事件binding.filterAll.setOnClickListener {updateFilter(FilterType.ALL)}binding.filterTypeA.setOnClickListener {updateFilter(FilterType.TYPE_A)}binding.filterTypeB.setOnClickListener {updateFilter(FilterType.TYPE_B)}binding.filterTypeC.setOnClickListener {updateFilter(FilterType.TYPE_C)}}private fun updateFilter(filterType: FilterType) {// 切换筛选条件时,先显示加载状态// 显示加载状态binding.swipeRefresh.visibility = View.GONEbinding.centerLoading.visibility = View.VISIBLEbinding.errorView.visibility = View.GONE// 更新筛选条件viewModel.updateFilter(filterType)}private fun setupSwipeRefresh() {binding.swipeRefresh.setOnRefreshListener {adapter.refresh()}}private fun setupRecyclerView() {// Android Recyclerview Paging3中,adapter.loadStateFlow中回调了LoadState.NotLoading,这时候为什么Recyclerview还是显示的老数据,有一个过渡动画后,才显示新数据// 所以这里 禁用RecyclerView的动画效果binding.recyclerView.itemAnimator = nullbinding.recyclerView.apply {layoutManager = LinearLayoutManager(this@LoadMoreActivity)adapter = this@LoadMoreActivity.adapter.withLoadStateFooter(footerAdapter)}}private fun setupViewModel() {viewModel = ViewModelProvider(this)[LoadMoreViewModel::class.java]lifecycleScope.launchWhenCreated {viewModel.pagingData.collect { pagingData ->adapter.submitData(pagingData)}}}private fun setupLoadStateListener() {lifecycleScope.launch {adapter.loadStateFlow.collectLatest { loadStates ->// 处理刷新状态binding.swipeRefresh.isRefreshing = loadStates.refresh is LoadState.Loading && binding.swipeRefresh.visibility == View.VISIBLE// 处理初始加载状态when (loadStates.refresh) {is LoadState.Loading -> {if (binding.swipeRefresh.isRefreshing) {// 如果是下拉刷新触发的加载,保持列表可见,仅显示刷新动画binding.swipeRefresh.visibility = View.VISIBLEbinding.centerLoading.visibility = View.GONEbinding.errorView.visibility = View.GONE} else {// 如果是初始加载,显示中央加载视图binding.swipeRefresh.visibility = View.GONEbinding.centerLoading.visibility = View.VISIBLEbinding.errorView.visibility = View.GONE}}is LoadState.Error -> {// 无论是筛选还是下拉刷新导致的错误,都应该正确显示错误状态// 如果列表中有数据,隐藏列表并显示错误视图binding.swipeRefresh.visibility = View.GONEbinding.centerLoading.visibility = View.GONEbinding.errorView.visibility = View.VISIBLE// 停止刷新动画binding.swipeRefresh.isRefreshing = false}is LoadState.NotLoading -> {// 加载完成,显示RecyclerView,隐藏其他视图binding.swipeRefresh.visibility = View.VISIBLEbinding.centerLoading.visibility = View.GONEbinding.errorView.visibility = View.GONE// 停止刷新动画binding.swipeRefresh.isRefreshing = false}}// 检查追加加载状态// 当append状态为NotLoading且endOfPaginationReached为true时,表示已加载所有数据val isEndOfList = loadStates.append is LoadState.NotLoading && (loadStates.append as? LoadState.NotLoading)?.endOfPaginationReached == trueif (isEndOfList) {// 当已显示全部数据时,可以显示提示//Toast.makeText(this@LoadMoreActivity, "已加载全部数据", Toast.LENGTH_SHORT).show()Log.d(TAG, "Append状态: ${loadStates.append}")Log.d(TAG, "Prepend状态: ${loadStates.prepend}")Log.d(TAG, "Refresh状态: ${loadStates.refresh}")Log.d(TAG, "endOfPaginationReached: ${(loadStates.append as? LoadState.NotLoading)?.endOfPaginationReached}")}}}// 添加滚动监听,确保能看到底部footerval layoutManager = binding.recyclerView.layoutManager as LinearLayoutManagerbinding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {super.onScrolled(recyclerView, dx, dy)// 如果滚动到最后一项if (layoutManager.findLastVisibleItemPosition() >= adapter.itemCount - 1) {Log.d(TAG, "已滚动到列表底部,总项数: ${adapter.itemCount}")}}})}private fun setupRetryButton() {binding.btnRetry.setOnClickListener {adapter.retry()}}
}

3.7 效果如下

加载更多、错误重试选择筛选向
在这里插入图片描述在这里插入图片描述

4. paging3库的相关概念

4.1 Paging 库的主要组件

  • PagingData - 用于存储分页数据的容器。每次数据刷新都会有一个相应的单独 PagingData
  • PagingSource - 直接负责从单一数据源(如本地数据库、内存缓存或网络 API)​​加载分页数据​​。
  • Pager.flow - 根据 PagingConfig 和一个定义如何构造实现的 PagingSource 的构造函数,构建一个 Flow<PagingData>
  • PagingDataAdapter - 一个用于在 RecyclerView 中呈现 PagingDataRecyclerView.AdapterPagingDataAdapter 可以连接到 Kotlin FlowLiveData、RxJava Flowable 或 RxJava ObservablePagingDataAdapter 会在页面加载时监听内部 PagingData 加载事件,并于以新对象 PagingData 的形式收到更新后的内容时,在后台线程中使用 DiffUtil 计算细粒度更新。
  • RemoteMediator - 有多种数据源,协调​​本地数据源(如数据库)和远程数据源(如网络 API)​​,用于在本地数据不足时触发远程加载,并将结果插入本地数据库。

4.2 PagingSource的getRefreshKey的作用

getRefreshKey 的主要作用是在刷新数据时,为新的分页请求提供一个锚点(Anchor),以确保刷新后能展示和刷新前相同位置的数据。当调用 PagingDataAdapter.refresh() 方法或者其他导致数据刷新的操作时,Paging3 会调用 getRefreshKey 方法获取一个键(Key),并依据这个键来决定从哪里开始加载新的数据。

4.2.1 PagingDataAdapter.refresh()不是重新加载数据吗,为什么刷新后要决定从哪里开始加载新的数据呢 ? 不是应该从第一页开始加载数据吗 ?

PagingDataAdapter.refresh() 的确是用于重新加载数据,但并非总是从第一页开始加载,这主要是为了优化用户体验和保持数据展示的连贯性。下面为你详细解释:

保持用户浏览位置

在实际应用场景中,用户在浏览数据列表时可能已经滚动到了列表的中间或者末尾位置。当调用 PagingDataAdapter.refresh() 刷新数据时,如果直接从第一页开始加载数据,列表会瞬间滚动到顶部,这会让用户丢失之前浏览的位置,体验感不佳。

借助 getRefreshKey 方法,Paging3 能够计算出当前可见数据的中间位置或者某个关键位置,然后从这个位置开始加载新的数据,这样刷新后列表依然能展示和刷新前相同位置的数据,用户可以继续顺畅地浏览。

数据更新的连续性

在某些情况下,数据可能只是部分更新,并非全部重置。例如,服务器端只更新了部分数据,而其他数据保持不变。这时,从当前位置开始加载新的数据可以避免不必要的重复加载,提高加载效率。

示例场景

假设你正在浏览一个新闻列表,已经滚动到了第 20 条新闻。此时,你点击刷新按钮来获取最新的新闻。如果直接从第一页开始加载数据,列表会回到顶部,你就需要重新滚动到第 20 条新闻的位置。而使用 getRefreshKey 方法,Paging3 会从第 20 条新闻附近开始加载新的数据,你可以继续浏览,无需重新定位。

代码示例

以下是一个简单的 getRefreshKey 方法示例,用于返回当前可见数据中间位置的键:

class MyPagingSource : PagingSource<Int, MyData>() {override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MyData> {// 实现数据加载逻辑return LoadResult.Page(data = emptyList(),prevKey = null,nextKey = null)}override fun getRefreshKey(state: PagingState<Int, MyData>): Int? {// 获取当前可见数据的中间位置的键return state.anchorPosition?.let { anchorPosition ->val anchorPage = state.closestPageToPosition(anchorPosition)anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)}}
}

在这个示例中,getRefreshKey 方法返回了当前可见数据中间位置的键,当调用 PagingDataAdapter.refresh() 时,Paging3 会根据这个键来决定从哪里开始加载新的数据。

4.2.2 我希望调用PagingDataAdapter.refresh()的时候,永远从第一页开始加载数据,应该怎么办 ?

若希望在调用 PagingDataAdapter.refresh() 时始终从第一页开始加载数据,可通过重写 PagingSource 的 getRefreshKey 方法,让其返回一个能使加载从第一页开始的键。一般而言,你可以让 getRefreshKey 方法返回 null,因为返回 null 会使 Paging3 从第一页开始加载数据。

4.3 PagingDataAdapter中的DiffUtil.ItemCallback的作用

DiffUtil.ItemCallback 的主要作用是计算新旧 PagingData 列表之间的差异,进而高效更新 RecyclerView 中的数据。它借助 DiffUtil 算法来对比两个列表,仅更新发生变化的部分,避免了整个列表的刷新,这在提升性能与用户体验方面效果显著。

具体用途
  • 识别数据变更:借助对比新旧数据列表,明确哪些数据项被添加、删除、移动或者更改。
  • 减少不必要的刷新:仅刷新发生变化的部分,而非重新加载整个列表,这样能减少视图重绘,优化性能。
  • 实现动画效果:在 RecyclerView 中实现平滑的动画效果,比如淡入淡出、滑动等,以此提升用户体验。
方法
  • DiffUtil.ItemCallback 是一个抽象类,你需要实现以下两个抽象方法:
    • areItemsTheSame:用来判断两个对象是否代表同一个数据项。通常是对比它们的唯一标识符(如 ID)。
    • areContentsTheSame:在 areItemsTheSame 返回 true 时被调用,用于判断两个对象的内容是否相同。若内容不同,RecyclerView 会更新该数据项的视图。

5. 功能拓展与优化

5.1 禁用列表动画,解决列表闪烁问题

当使用Paging3时,有时数据更新会导致列表闪烁。解决这个问题可以禁用RecyclerView的动画:

// 禁用RecyclerView的动画效果
binding.recyclerView.itemAnimator = null

5.2 处理没有更多数据的显示

默认情况下,displayLoadStateAsItem只判断了LoadState.Loading和LoadState.Error,如果要LoadState.NotLoading也能回调onBindViewHolder方法,需要重写displayLoadStateAsItem方法。

// 修改LoadStateAdapter中的方法
override fun displayLoadStateAsItem(loadState: LoadState): Boolean {return loadState is LoadState.Loading || loadState is LoadState.Error || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached)
}

5.3 在Compose中使用Paging3

在Compose中使用Paging3,比如Recyclerview,会更加简单,示例如下

@Composable
fun PagingDemoPage(viewModel: PagingViewModel = PagingViewModel()) {val pagingItems = viewModel.pagingFlow.collectAsLazyPagingItems()LazyColumn(modifier = Modifier.fillMaxSize()) {items(pagingItems.itemCount) { index ->Text(text = pagingItems[index] ?: "Loading...")}}
}

相关文章:

Android 使用Paging3 实现列表分页加载、下拉刷新、错误重试、筛选功能

Android 使用Paging3 实现列表加载 Paging3是Android Jetpack组件库中的分页加载库&#xff0c;它可以帮助开发者轻松实现列表数据的分页加载功能。本文将逐步讲解如何使用Paging3库实现一个带有加载更多、下拉刷新、错误重试、筛选功能的列表页面。 最终效果如下 加载更多、…...

SpringBoot2集成xxl-job详解

官方教程 搭建调度中心 Github Gitee 注&#xff1a;版本3.x开始要求Jdk17&#xff1b;版本2.x及以下支持Jdk1.8。如对Jdk版本有诉求&#xff0c;可选择接入不同版本 clone源代码执行xxl-job\doc\db\tables_xxl_job.sql # # XXL-JOB v2.4.1 # Copyright (c) 2015-present, x…...

洛图报告中的 FSHD 是什么?—— 解密九天画芯推动的三色光源显示技术

目录 一、洛图报告新焦点&#xff1a;FSHD 为何成为显示产业重要突破方向&#xff1f; &#xff08;一&#xff09;洛图报告核心结论&#xff1a;从技术突围到产业重构 二、技术解析&#xff1a;FSHD 如何重构显示底层逻辑&#xff1f; &#xff08;一&#xff09;物理架构…...

关于数据库查询速度优化

本人接手了一个关于项目没有任何文档信息的代码&#xff0c;代码也没有相关文档说明信息&#xff01;所以在做数据库查询优化的时候不敢改动。 原因1&#xff1a; 老板需要我做一个首页的统计查询。明明才几十万条数据&#xff0c;而且我加了筛选条件为什么会这么慢&#xff…...

两数相加(2)

2. 两数相加 - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a; class Solution { public:ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {ListNode * dummy new ListNode(0);int carry 0;ListNode * head dummy;while (l1 ! nullptr || l2 ! nullptr ||…...

车载学习(6)——CAPL(1)一些基础知识

1.一些CAPL的知识 CAPL通讯访问编程语言&#xff08;Commmunication Access Programming language&#xff09; CAPL的用途 ECU的仿真实现&#xff08;报文的收发、数据的处理&#xff09;实现总线日志纪录的控制实现总线数据自动化分析实现ECU功能的自动化测试实现ECU报文发…...

C++笔记-set和map的使用(包含multiset和multimap的讲解)

1.序列式容器和关联式容器 前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等&#xff0c;这些容器统称为序列式容器&#xff0c;因为逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值之间一般没有紧密的关联关系&#xff0…...

GLPK(GNU线性规划工具包)介绍

GLPK全称为GNU Linear Programming Kit(GNU线性规划工具包)&#xff0c;可从 https://sourceforge.net/projects/winglpk/ 下载源码及二进制库&#xff0c;最新版本为4.65。也可从 https://ftp.gnu.org/gnu/glpk/ 下载&#xff0c;仅包含源码&#xff0c;最新版本为5.0。 GLPK是…...

技嘉主板BIOS升级

截图可能不同的BIOS长的不一样&#xff0c;但大概相同&#xff0c;BIOS不下错&#xff0c;逐个版本升级就没问题 准备工作 准备工作一个fat32格式的U盘&#xff0c;用来存放BIOS确认主板型号 和当前BIOS版本 方式1&#xff1a;去系统信息里面 方式2&#xff1a; 去BIOS设置…...

自定义类型:结构体进阶

一、结构体类型的声明 前⾯我们在学习操作符和初阶结构体的时候&#xff0c;已经学习了结构体的知识&#xff0c;这⾥稍微复习⼀下。 1.1结构体的回顾 结构是⼀些值的集合&#xff0c;这些值称为成员变量。结构的每个成员可以是不同类型的变量。 1.2结构的声明 struct tag…...

linux perf top分析系统性能

1,看到内核宏是否支持perf功能 perf top -g 查看linux 内核宏定义 CONFIG_PERF_EVENTS=y CONFIG_HAVE_PERF_EVENTS=y CONFIG_FRAME_POINTER=y # 确保帧指针支持以捕获完整堆栈 2,使用perf top -g 报错 Access to performance monitoring and observability operations is l…...

串口屏调试 1.0

http://wiki.tjc1688.com 先把商家的链接贴过来 淘晶驰T1系列3.2寸串口屏tft液晶屏显示屏HMI触摸屏超12864液晶屏 这是主包的型号 打开这个玩意 有十个基本的功能区 新建工程 在界面的右边&#xff0c;指令一定要写在page前面&#xff0c;这里的波特率等等什么的都可以…...

Python面向对象编程:初识类与对象

面向对象编程(OOP)是Python编程中最重要的范式之一。本文将从零开始介绍Python中类和对象的基本概念&#xff0c;帮助初学者快速掌握面向对象编程的核心思想。 一、什么是面向对象编程&#xff1f; 面向对象编程(Object-Oriented Programming)是一种以"对象"为核心…...

局域网常用的测速工具,Iperf3使用教程

目录 下载方式 Windows Linux 使用方法&#xff1a;测试局域网带宽 步骤一&#xff1a;服务端准备 步骤二&#xff1a;客户端发起连接 步骤三&#xff1a;查看结果 参数说明 1. Iperf常用参数&#xff08;测试够用&#xff09; 2. 通用参数&#xff08;Server端和Cli…...

[架构之美]linux常见故障问题解决方案(十九)

[架构之美]linux下常见故障问题解决方案 一&#xff0c;文本文件忙 问题一&#xff1a;rootwh-VMware-Virtual-Platform:/home/hail# cp /root/containerd/bin/* /usr/bin/ cp: 无法创建普通文件 ‘/usr/bin/containerd’: 文本文件忙 在Linux系统中遇到“文本文件忙”错误时…...

Java与Go语言对比教程

Java vs. Go&#xff1a;程序员的双节棍与瑞士军刀之战 &#x1f3af; 先看一张灵魂对比图 &#x1f916; Java老大哥 &#x1f680; Go小钢炮 出生年份 1995&#xff08;中年稳健&#xff09; 2009&#xff08;年轻活力&…...

计算机大类专业数据结构下半期实验练习题

1068: 图的按录入顺序深度优先搜索 #include"iostream" using namespace std; #include"cstring" int visited[100]; char s[100]; int a[100][100]; int n; void dfs(int k,int n) {if(visited[k]0){visited[k]1;cout<<s[k];for(int i0;i<n;i){i…...

Android 关闭Activity切换过渡动画

Android 9.0以前关闭过渡动画效果只需要把开发者模式中过渡动画缩放设为0就可以。也就是把def_window_transition_scale改为0% frameworks/base/packages/SettingsProvider/res/values/defaults.xml <fraction name"def_window_transition_scale">100%<…...

iperf3的介绍与舒勇

在 CentOS 7 中使用 iperf3 进行网络性能测试&#xff0c;分为客户端和服务器两部分。以下是使用步骤&#xff1a; 1. 安装 iperf3 首先&#xff0c;你需要在 CentOS 7 上安装 iperf3。可以通过以下命令进行安装&#xff1a; sudo yum install epel-release sudo yum install…...

Python 包管理新选择:uv

在 Python 中,uv 是由 Astral 公司开发的 Rust 高性能包管理工具,旨在替代传统 pip 并提供更快的依赖管理、Python 版本控制等功能。 https://github.com/astral-sh/uv 在 github 上已有 53k star 一、uv 的核心优势 极速性能:比 pip 快 10-100 倍。多版本 Python 管理:支…...

从需求到用例的AI路径:准确率与挑战

用工作流生成测试用例和自动化测试脚本&#xff01; 引言&#xff1a;用例的黄金起点 在软件工程中&#xff0c;“测试用例”是连接需求理解与质量保障之间的关键桥梁。一份高质量的测试用例&#xff0c;不仅是验证功能实现是否符合需求的工具&#xff0c;更是产品风险感知、用…...

JavaSE核心知识点02面向对象编程02-06(泛型)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 JavaSE核心知识点02面向对象编程02-06&#…...

xml与注解的区别

功能xml配置注解定义bean bean标签 id属性 class属性 Component Controller Service Repository ComponentScan 设置依赖注入 setter注入&#xff08;set方法&#xff09; 构造器注入&#xff08;构造方法&#xff09; Autowired Qualifier Value 配置第三方bean bean标签 静…...

进阶 DFS 学习笔记

字数&#xff1a;12017字。 文章盗的图注明了出处&#xff0c;全部出自 y 总的算法提高课。 不知道为啥这个时候才开始学这个东西&#xff0c;好像是很多同龄人都已经学完了。 进阶 DFS 具体来说好几个东西&#xff0c;所以可能内容有一些些多。 默认 DFS 和 BFS 已经掌握了…...

计算机设计大赛山东省赛区软件开发赛道线上答辩复盘

流程回顾&#xff1a; 1.抽签顺序&#xff1a; 抽签顺序并不一定代表是最终顺序&#xff0c;要注意看通知不要遗漏。 2.答辩形式&#xff1a; 线上答辩&#xff0c;加入腾讯会议&#xff0c;进会议时自己的备注是作品编号&#xff0c;等轮到自己组答辩时主持人会把人拉进来…...

第7次课 栈A

课堂学习 栈&#xff08;stack&#xff09; 是一种遵循先入后出逻辑的线性数据结构。 我们可以将栈类比为桌面上的一摞盘子&#xff0c;如果想取出底部的盘子&#xff0c;则需要先将上面的盘子依次移走。我们将盘子替换为各种类型的元素&#xff08;如整数、字符、对象等&…...

TXT编码转换工具iconv

iconv.exe是实现TXT编码转换的命令行工具&#xff0c;支持几百种编码格式的转换&#xff0c;利用它可以在自主开发程序上实现TXT文档编码的自动转换。 一、命令参数格式 Usage: iconv [-c] [-s] [-f fromcode] [-t tocode] [file ...] or: iconv -l 二、转换的示例 将UTF-8…...

基于Spring Boot + Vue的高校心理教育辅导系统

一、项目背景介绍 随着高校对学生心理健康教育的重视&#xff0c;传统的人工心理辅导与测评模式已经难以满足广大师生的个性化需求。为了提高心理服务的效率、便捷度和覆盖范围&#xff0c;本项目开发了一个高校心理教育辅导系统&#xff0c;集成心理评测、辅导预约、留言交流…...

关于甲骨文(oracle cloud)丢失MFA的解决方案

前两年&#xff0c;申请了一个招商的多币种信用卡&#xff0c;然后就从网上撸了一个oracle的免费1h1g的服务器。 用了一段时间&#xff0c;人家要启用MFA验证。 啥叫MFA验证&#xff0c;类似与短信验证吧&#xff0c;就是绑定一个手机&#xff0c;然后下载一个app&#xff0c;每…...

Linux系统管理与编程17:自动化部署ftp服务

兰生幽谷&#xff0c;不为莫服而不芳&#xff1b; 君子行义&#xff0c;不为莫知而止休。 #virtual用户管理&#xff1a;passerbyA、captain和admin三个虚拟用户 # passerbyA只能看&#xff0c;captain可看读写上传&#xff0c;但不能删除。admin全部权限 [rootshell shell]…...

C++STL——stack,queue

stack与queue 前言容器适配器deque 前言 本篇主要讲解stack与queue的底层&#xff0c;但并不会进行实现&#xff0c;stack的接口 queue的接口 &#xff0c;关于stack与queue的接口在这里不做讲解&#xff0c;因为通过前面的对STL的学习&#xff0c;这些接口都是大同小异的。 …...

HC-SR04超声波测距传感器

1.基本信息 供电电压5v,测量范围2cm~400cm,测量精度正负3mm&#xff0c;超声波频率40khz 2.连接引脚&#xff1a; 3.工作原理 TRIG引脚发送至少10us的高电平信号&#xff0c;ECHO引脚负责接受信号&#xff1b; 接受方式&#xff1a;计算测量高电平持续的时间&#xff0c;从一…...

内存安全暗战:从 CVE-2025-21298 看 C 语言防御体系的范式革命

引言 2025 年 3 月&#xff0c;美国 CERT 发布的《年度漏洞报告》揭示了触目惊心的数据&#xff1a;C/C 相关漏洞占全年高危漏洞的 68%&#xff0c;其中内存安全问题贡献了 92% 的远程代码执行风险。当 CVE-2025-21298 漏洞在某工业控制软件中被利用&#xff0c;导致欧洲某核电…...

Linux笔记---System V共享内存

1. System V共享内存简介 System V共享内存是一种在Linux系统中用于进程间通信的机制。顾名思义&#xff0c;就是申请一段可供多个进程共享的内存&#xff0c;以用于进程间通信&#xff0c;相对于管道机制要更加直接。 1.1 原理 System V共享内存通过创建和使用一个特定的IP…...

MySQL 1366 - Incorrect string value:错误

MySQL 1366 - Incorrect string value:错误 错误如何发生发生原因&#xff1a; 解决方法第一种尝试第二种尝试 错误 如何发生 在给MySQL添加数据的时候发生了下面的错误 insert into sys_dept values(100, 0, 0, 若依科技, 0, 若依, 15888888888, ryqq.com, 0,…...

慈缘基金会“蝴蝶飞”助西藏女孩白玛卓嘎“折翼重生”

历经六个月、178天的艰难治疗&#xff0c;来自西藏拉萨的15岁女孩白玛卓嘎&#xff0c;终于在4月底挺直脊梁&#xff0c;带着自信的笑容踏上了回家的路。这场跨越雪域高原与首都北京的“生命蜕变之旅”&#xff0c;不仅改写了这位藏族少女的人生轨迹&#xff0c;更见证了公益力…...

【生存技能】ubuntu 24.04 如何pip install

目录 原因解决方案说明 在接手一个新项目需要安装python库时弹出了以下提示: 原因 这个报错是因为在ubuntu中尝试直接使用 pip 安装 Python 包到系统环境中&#xff0c;ubuntu 系统 出于稳定性考虑禁止了这种操作 这里的kali是因为这台机器的用户起名叫kali,我也不知道为什么…...

TDengine 在智能制造中的核心价值

简介 智能制造与数据库技术的深度融合&#xff0c;已成为现代工业技术进步的一个重要里程碑。随着信息技术的飞速发展&#xff0c;智能制造已经成为推动工业转型升级的关键动力。在这一进程中&#xff0c;数据库技术扮演着不可或缺的角色&#xff0c;它不仅承载着海量的生产数…...

代码随想录第41天:图论2(岛屿系列)

一、岛屿数量&#xff08;Kamacoder 99&#xff09; 深度优先搜索&#xff1a; # 定义四个方向&#xff1a;右、下、左、上&#xff0c;用于 DFS 中四向遍历 direction [[0, 1], [1, 0], [0, -1], [-1, 0]]def dfs(grid, visited, x, y):"""对一块陆地进行深度…...

C语言复习--柔性数组

柔性数组是C99中提出的一个概念.结构体中的最后⼀个元素允许是未知大小的数组&#xff0c;这就叫做柔性数组成员。 格式大概如下 struct S { int a; char b; int arr[];//柔性数组 }; 也可以写成 struct S { int a; char b; int arr[0];//柔性数组 }; …...

《Python星球日记》 第55天:迁移学习与预训练模型

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 目录 一、迁移学习基础1. 什么是迁移学习&#xff1f;2. 迁移学习的优势3. 迁移学习的…...

Python项目75:PyInstaller+Tkinter+subprocess打包工具1.0(安排 !!)

这个打包工具包含以下功能&#xff1a; 1.主要功能&#xff1a;选择Python脚本文件&#xff0c;设置打包选项&#xff08;单文件打包、无控制台窗口&#xff09;&#xff0c;自定义程序图标&#xff0c;指定输出目录&#xff0c;实时显示打包日志。 2.自适应布局改进&#xff…...

互联网大厂Java面试实录:从基础到微服务的深度考察

互联网大厂Java面试实录&#xff1a;从基础到微服务的深度考察 面试场景 面试官&#xff1a;风清扬&#xff08;严肃且技术深厚&#xff09; 求职者&#xff1a;令狐冲&#xff08;技术扎实但偶尔含糊&#xff09; 第一轮&#xff1a;Java基础与框架 风清扬&#xff1a;令狐…...

学习黑客5 分钟深入浅出理解Linux进程管理

5 分钟深入浅出理解Linux进程管理 &#x1f5a5;️ 大家好&#xff01;今天我们将探索Linux系统中的进程管理——这是理解系统运行机制和进行安全分析的基础知识。在TryHackMe平台上进行网络安全学习时&#xff0c;了解进程如何工作以及如何监控和控制它们&#xff0c;对于识别…...

Kubernetes应用发布方式完整流程指南

Kubernetes&#xff08;K8s&#xff09;作为容器编排领域的核心工具&#xff0c;其应用发布流程体现了自动化、弹性和可观测性的优势。本文将通过一个Tomcat应用的示例&#xff0c;详细讲解从配置编写到高级发布的完整流程&#xff0c;帮助开发者掌握Kubernetes应用部署的核心步…...

JVM——即时编译器的中间表达形式

中间表达形式&#xff08;IR&#xff09;&#xff1a;编译器的核心抽象层 1. IR的本质与作用 在编译原理的体系中&#xff0c;中间表达形式&#xff08;Intermediate Representation, IR&#xff09;是连接编译器前端与后端的桥梁。前端负责将源代码转换为IR&#xff0c;而后…...

Js 判断浏览器cookie 是否启用

验证时 google浏览器 135.0.7049.117 不生效 cookie.html <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><title>Cookie 检测</title> </head> <body><h1>检测是否启用 Cookie<…...

数字相机的快门结构

数字相机(DC/DSLR等)的快门结构和传统相机有所不同,除了机械快门以外,还存在电子快门,实际上是二者的混合体。我写这篇文章大概介绍一下数字相机的快门结构,希望能抛砖引玉。 要讨论数字相机的快门结构,首先先要了解一下数字相机的结构分类,根据成像原理不同,数字相机大…...

LeetCode --- 448 周赛

题目列表 3536. 两个数字的最大乘积 3537. 填充特殊网格 3538. 合并得到最小旅行时间 3539. 魔法序列的数组乘积之和 一、两个数字的最大乘积 由于数据都是正数&#xff0c;所以乘积最大的两个数&#xff0c;本质就是找数组中最大的两个数即可&#xff0c;可以排序后直接找到…...

添加物体.

在cesium中我们可以添加物体进入地图.我们以广州塔为例 //生成广州塔的位置var position2 Cesium.Cartesian3.fromDegrees(113.3191,23.109,100)viewer.camera.setView({//指定相机位置destination: position2, 运行后如图 我们使用cesium官网提供的代码为广州塔在地图上标点…...