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 ?: 0
            val 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.id
            override 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">

    <TextView
        android: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: MainViewModel
    private 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 = 5
        
        private const val TAG = "ExamplePagingSource"
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ExampleItem> {
        return try {

            //模拟网络请求耗时
            delay(2000)

            val pageIndex = params.key ?: 0
            val pageSize = params.loadSize

            if (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 + it
                val itemType = filterType

                ExampleItem(
                    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 - 1

            val nextKey = if (allItems.isNotEmpty() && !isLastPage) pageIndex + 1 else null
            Log.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)
    
    // 每次筛选条件变化时,重新创建Pager
    val 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.id
            override 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}")

        // 根据不同状态设置UI
        when (loadState) {
            is LoadState.Loading -> {
                holder.binding.loadingProgress.visibility = View.VISIBLE
                holder.binding.loadingText.text = "加载中..."
                holder.binding.loadingText.isClickable = false
            }
            is LoadState.Error -> {
                holder.binding.loadingProgress.visibility = View.GONE
                holder.binding.loadingText.text = "加载失败,请重试"
                holder.binding.loadingText.isClickable = true
                holder.binding.loadingText.setOnClickListener { retry() }
            }
            is LoadState.NotLoading -> {
                holder.binding.loadingProgress.visibility = View.GONE
                if (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 = false
                holder.binding.loadingText.setOnClickListener(null)
            }
        }
    }

    // 关键:确保当没有更多数据时也显示footer
    override 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">

    <!-- 筛选器布局 -->
    <HorizontalScrollView
        android: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.ChipGroup
            android: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.Chip
                android: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.Chip
                android: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.Chip
                android: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.Chip
                android: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.SwipeRefreshLayout
        android: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.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

    <!-- 中央加载进度条 -->
    <ProgressBar
        android: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"/>

    <!-- 错误重试布局 -->
    <LinearLayout
        android: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">

        <ImageView
            android:layout_width="60dp"
            android:layout_height="60dp"
            android:src="@android:drawable/ic_dialog_alert"
            android:contentDescription="错误图标"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="加载失败"/>

        <Button
            android: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: LoadMoreViewModel
    private val adapter = ExampleAdapter()
    private val footerAdapter = LoadStateAdapter { adapter.retry() }
    private lateinit var binding: ActivityLoadMoreBinding

    companion object {
        private const val TAG = "LoadMoreActivity"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        
        // 初始化ViewBinding
        binding = 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.GONE
        binding.centerLoading.visibility = View.VISIBLE
        binding.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 = null
        binding.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.VISIBLE
                            binding.centerLoading.visibility = View.GONE
                            binding.errorView.visibility = View.GONE
                        } else {
                            // 如果是初始加载,显示中央加载视图
                            binding.swipeRefresh.visibility = View.GONE
                            binding.centerLoading.visibility = View.VISIBLE
                            binding.errorView.visibility = View.GONE
                        }
                    }
                    is LoadState.Error -> {
                        // 无论是筛选还是下拉刷新导致的错误,都应该正确显示错误状态
                        // 如果列表中有数据,隐藏列表并显示错误视图
                        binding.swipeRefresh.visibility = View.GONE
                        binding.centerLoading.visibility = View.GONE
                        binding.errorView.visibility = View.VISIBLE
                        
                        // 停止刷新动画
                        binding.swipeRefresh.isRefreshing = false
                    }
                    is LoadState.NotLoading -> {
                        // 加载完成,显示RecyclerView,隐藏其他视图
                        binding.swipeRefresh.visibility = View.VISIBLE
                        binding.centerLoading.visibility = View.GONE
                        binding.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 == true
                
                if (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}")
                }
            }
        }
        
        // 添加滚动监听,确保能看到底部footer
        val layoutManager = binding.recyclerView.layoutManager as LinearLayoutManager
        binding.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...")
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

氦客

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值