Paging3 系列--Paging3的基本概念和使用

4,684 阅读11分钟

Paging3是什么

Jetpack Paging 3 是 Android 开发中一种强大而灵活的库,专门用于处理大量数据列表的分页加载。在现代移动应用中,数据量庞大的列表已经成为常态,而有效地加载和展示这些数据对于提供良好的用户体验至关重要。Jetpack Paging 3 提供了一种简单且可扩展的方法,帮助开发者高效地实现分页加载功能,同时解决了传统分页库的一些常见问题,如内存管理、预取和错误处理。

Paging3系列文章,将介绍 Jetpack Paging 3 的基本概念和工作原理,详细解释其核心组件和用法,并将探讨如何实施 Paging 3,包括创建分页数据源、配置适配器以及将数据展示在 RecyclerView 中。此外,我们还将探讨一些高级功能和定制化选项,如网络状态处理和自定义分页策略等,以满足各种应用场景的需求。

Paging 3 的主要组件

Jetpack Paging 3 是 提供了一套用于加载和展示分页数据的组件。以下是 Jetpack Paging 3 的主要组件:

  • PagingData

  • PagingDataSource

  • Pager

  • PagingDataAdapter

  • RemoteMediator

我们先上图,大致了解下各自的作用:

image.png

整体分为数据展示、数据控制、数据获取三个部分,而RemoteMediator没有在图中体现,是因为我自己平时也没怎么用到,哈哈,可以简单了解下即可,下面我们来具体了解一下每个组件都是做什么的:

  1. PagingSource:

PagingSource 是 Jetpack Paging 3 中的核心组件之一,用于定义数据的来源和加载方式。开发者需要实现 PagingSource 抽象类,并在其中指定如何从数据源中加载特定页的数据。PagingSource 通常用于与网络 API 或本地数据库进行交互,获取分页数据。

  1. Pager:

Pager 是用于创建分页数据流的类。通过 Pager,开发者可以将 PagingSource 与其他配置参数(如分页大小、预取距离等)结合起来,创建用于加载和展示分页数据的 PagingData 数据流。Pager 提供了多个静态方法,用于创建不同类型的 PagingData 数据流。

  1. PagingData:

PagingData 是 Jetpack Paging 3 中用于表示分页数据的类。它是一个泛型类,可以容纳各种类型的分页数据。PagingData 是一个不可变的数据类,可以在 RecyclerView 中进行展示,并具有与分页相关的特性,如加载状态、分页状态等。

  1. PagingDataAdapter:

PagingDataAdapter 是 RecyclerView.Adapter 的子类,专门用于展示 PagingData 数据流中的分页数据。PagingDataAdapter 提供了内置的数据差异计算和局部刷新机制,使得在 RecyclerView 中展示分页数据变得更加高效和简单。它还提供了加载状态和错误处理等功能。

  1. LoadStateAdapter:

LoadStateAdapter 是一个用于展示加载状态的 RecyclerView.Adapter 的子类。它可以与 PagingDataAdapter 结合使用,用于展示分页数据的加载状态,如加载中、加载错误等。LoadStateAdapter 可以显示自定义的加载状态布局,并根据加载状态的变化自动更新 UI。

  1. RemoteMediator:

RemoteMediator 是用于处理远程数据加载和数据库插入的接口。当 PagingSource 加载远程数据时,RemoteMediator 可以在加载完成后将数据插入本地数据库,并提供信息以支持分页和数据持久化。RemoteMediator 是实现离线缓存和数据持久化的关键组件之一。

这些组件共同构成了 Jetpack Paging 3 的核心,提供了方便而强大的工具和组件,用于实现分页数据的加载、展示和缓存。开发者可以根据具体需求使用这些组件来构建功能丰富且高效的分页加载体验。

Paging3的基本使用流程

  1. 添加 Paging 3 依赖库
  2. 创建 PagingSource
  3. 配置 PagingDataAdapter
  4. 将数据展示在 RecyclerView 中

添加Paging3的依赖

dependencies {
    // 其他依赖项...
    implementation "androidx.paging:paging-runtime:3.1.1"
    // 可选kotlin扩展
    implementation "androidx.paging:paging-runtime-ktx:3.1.1"
}

依赖库 "androidx.paging:paging-runtime-ktx" 是 Paging 3 的 Kotlin 扩展库,它提供了一些针对 Kotlin 开发的便利性函数和扩展。常见的作用和使用场景有:

  1. 提供 Kotlin 扩展函数:该依赖库为 Paging 3 添加了一些 Kotlin 扩展函数,简化了与 Paging 相关的代码编写。例如,它提供了 PagingData<T>.cachedIn() 扩展函数,用于在流的生命周期内缓存 PagingData,以提高数据的重用性。
  2. 支持 Kotlin 协程:Paging 3 本身就是与 Kotlin 协程紧密集成的库,但 "androidx.paging:paging-runtime-ktx" 进一步增强了与协程的集成。它提供了 PagingDataFlow 类,用于将 PagingData 转换为 Kotlin Flow 类型,使得在使用协程进行数据处理时更加方便和自然。
  3. 简化数据加载和处理:依赖库中的一些函数和扩展可以简化数据加载和处理的代码。例如,它提供了 PagingDataAdapter.submitData() 扩展函数,用于将新的 PagingData 数据提交给适配器,并自动计算数据的差异并进行局部刷新。

虽然 是可选的依赖库,但它提供了一些方便的函数和扩展,可以提升使用 Paging 3 的开发效率和代码的可读性。所以就直接依赖上吧,哈哈。

创建PagingSource

创建 PagingSource 是使用 Jetpack Paging 3 实现分页加载的关键步骤之一。

当创建 PagingSource 时,以下是一个包含代码示例和说明的详细描述:

自定义 PagingSource 类:

首先,创建一个自定义的 PagingSource 类,该类继承自 PagingSource 抽象类。命名和定义类根据你的数据源类型和加载逻辑需求。

class MyPagingSource(private val apiService: ApiService) : PagingSource<Int, ListItem>() {
    // ...
}

在 PagingSource<Int, ListItem> 中,两个参数的含义分别是:

  • 第一个参数 Int:表示页的标识符。在 Paging 3 中,每个页都需要一个唯一的标识符来识别它。通常情况下,这个标识符可以是整数类型,表示页的编号或索引。在加载数据时,我们可以根据这个标识符来确定要加载的是哪一页的数据。

  • 第二个参数 ListItem:表示加载的数据项的类型。这个类型可以是你自定义的任何数据类型,根据你的需求而定。在分页加载过程中,每个加载的数据项都属于这个类型。

继承PagingSource需要实现两个方法:

  • load: 负责加载特定页的数据,返回一个 LoadResult 对象

  • getRefreshKey:用于确定刷新操作的键值,它返回一个用于标识刷新操作的键。该键将用于在数据集发生更改时进行比较,以检测是否需要进行刷新。

    •   getRefreshKey 方法大多数场景用不到,直接返回null就好了

实现 load 方法

在自定义的 PagingSource 类中,实现 load 方法。该方法负责加载特定页的数据,负责根据传入的 LoadParams 对象加载特定页的数据,并返回一个 LoadResult 对象。

  • LoadParams 包含了当前请求的加载信息,例如请求的页数、请求的加载大小等

  • LoadResult 是一个包装类,用于封装加载结果。它可以是 LoadResult.Page、LoadResult.Error

    • LoadResult.Page 表示成功加载了一页数据,需要提供加载的数据列表和可选的前一页和后一页的信息
    • LoadResult.Error 表示加载数据时遇到了错误,需要提供一个Throwable对象。
class MyPagingSource(private val apiService: ApiService) : PagingSource<Int, ListItem>() {
    override fun getRefreshKey(state: PagingState<Int, ListItem>): Int? {
        return null
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ListItem> {
        try {
            // 获取请求的页数
            val pageNumber = params.key ?: 1

            var listItems = mutableListOf<ListItem>()
            // 加载数据
            apiService.getItems(pageNumber, params.loadSize)
                .onSuccess {
                    listItems.addAll(it)
                }.onFailure {
                    // 返回加载错误的 LoadResult.Error 对象
                    return LoadResult.Error(it)
                }

            // 设置前一页和下一页的信息
            val prevKey = if (pageNumber > 1) pageNumber - 1 else null
            val nextKey = if (listItems.isNotEmpty()) pageNumber + 1 else null
            
            
            // 构建 LoadResult.Page 对象并返回
            return LoadResult.Page(
                data = listItems,
                prevKey = prevKey,
                nextKey = nextKey
            )
        } catch (e: Exception) {
            // 返回加载错误的 LoadResult.Error 对象
            return LoadResult.Error(e)
        }
    }
}

在load方法中,我们需要处理以下几件事情:

  • 编写具体的数据加载逻辑:在 load 方法中,根据加载参数实现具体的数据加载逻辑。例如,使用网络请求获取特定页的数据

  • 处理数据加载状态和错误:在数据加载过程中,根据需要处理加载状态和错误。例如,可以在加载开始时发送加载中的状态,加载成功后发送加载完成的状态,加载出错时发送错误状态

  • 设置下一页或者前一页的key值: 在 LoadResult.Page 中,prevKey 和 nextKey 分别代表了前一页和后一页的页标识符。它们用于指示是否存在前一页和后一页的数据,为null则表示没有数据

创建Pager

创建 Pager 是使用 Jetpack Paging 3 的关键步骤之一。Pager 类负责将 PagingSource 与界面进行绑定,并提供可供界面使用的流式数据。下面是关于如何创建 Pager 的详细描述以及相应的代码示例:

要创建 Pager,首先需要指定数据源 PagingSource、加载配置 PagingConfig 和可选的初始化数据。然后,可以通过调用 Pager.flow 方法来获取用于观察分页数据的流。

下面是一个示例,展示了如何创建 Pager 并获取用于观察分页数据的流:

class MyViewModel: ViewModel() {
    
    private val apiService = MockApiService()
    
    val myPager = Pager(
        config = PagingConfig(pageSize = 20, prefetchDistance = 2, initialLoadSize = 20),
        initialKey = 1,
        pagingSourceFactory = {
            MyPagingSource(apiService)
        }
    
    )
}

在上述示例中,我们首先创建了自定义的 PagingSource 对象 pagingSource,然后创建了 PagingConfig 对象 pagingConfig,用于配置每页加载的项数等参数:

  • pageSize:我们指定每页加载多少项数据
  • prefetchDistance:预取下一页数据的距离,不能为0,否则不会拉取下一页数据;
  • initialLoadSize:初始加载多少项数据,跟pageSize保持一致就好

接下来,我们使用 Pager 的构造函数创建了 Pager 对象 pager。其中,config 参数接收 pagingConfig,pagingSourceFactory 参数接收一个返回 pagingSource 的函数,initialKey 参数是可选的,用于指定初始页的键值。

下面我们介绍一种错误的创建Pager的方式:

private val pagingSource = MyPagingSource(apiService) // 创建自定义的 PagingSource
private val pagingConfig = PagingConfig(pageSize = 20,prefetchDistance = 2, initialLoadSize = 20) // 创建 PagingConfig,设置每页加载的项数

val pager = Pager(
    config = pagingConfig,
    pagingSourceFactory = { pagingSource },
    initialKey = 1 // 可选的初始化数据,指定初始页的键值
)

使用此种方式创建Pager时,正常使用时可能没有问题,但当你为了刷新列表而调用adapter.refresh()时,应用会崩溃,报错如下:


java.lang.IllegalStateException: An instance of PagingSource was re-used when Pager expected to create a new
instance. Ensure that the pagingSourceFactory passed to Pager always returns a
new instance of PagingSource.
        at androidx.paging.PageFetcher.generateNewPagingSource(PageFetcher.kt:193)
        at androidx.paging.PageFetcher.access$generateNewPagingSource(PageFetcher.kt:31)
        at androidx.paging.PageFetcher$flow$1$2.invokeSuspend(PageFetcher.kt:66)

根据报错信息我们可以知道,当我们调用adapter.refresh()时,Paging3会去重新创建一个PagingSource,并且不允许我们重复使用同一个PagingSource实例

配置PageDataAdapter

要创建 PagingDataAdapter,你需要继承 PagingDataAdapter 类,并实现 onCreateViewHolder 和 onBindViewHolder 方法来创建和绑定数据项的视图。下面是创建 PagingDataAdapter 示例代码:

class MyPagingDataAdapter: PagingDataAdapter<ListItem, MyPagingDataAdapter.ListItemViewHolder>(ItemComparator) {

    // 自定义 ItemComparator,用于比较数据项
    object ItemComparator : DiffUtil.ItemCallback<ListItem>() {
        override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem): Boolean {
            return oldItem == newItem
        }
    }

    inner class ListItemViewHolder(val binding: ItemListBinding): RecyclerView.ViewHolder(binding.root)

    override fun onBindViewHolder(holder: ListItemViewHolder, position: Int) {
        val item = getItem(position)
        holder.binding.index.text = item?.id.toString()
        holder.binding.tvName.text = item?.name
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemViewHolder {
        return ListItemViewHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, true))
    }
}

在上述示例中,我们创建了一个名为 MyPagingDataAdapter 的自定义 PagingDataAdapter 类。在 PagingDataAdapter 的构造函数中,我们传递了 ListItem 类型和 ListItemViewHolder 类型。

  • onCreateViewHolder和onBindViewHolder的重写不必多说,但是需要注意的是,我们在自定义PagingDataAdapter中并未传入任何数据流,但是我们可以通过getItem()方法拿到对应的数据项,这也是PagingDataAdapter内部封装的一个API

  • 另外可以看到,我们定义了个ItemComparator 对象,这是因为PagingDataAdapter要求我们必须传入DiffUtil.ItemCallback<T>对象,用于判断数据差异,从而进行局部刷新,所以,我们定义了一个 ItemComparator 对象,用于比较数据项。

通过以上步骤,我们成功创建了自定义的 PagingDataAdapter,并实现了必要的方法来创建和绑定数据项的视图。

将数据展示在 RecyclerView 中

至此到了使用Paging3的最后一步,展示数据:

将 PagingData 设置给 RecyclerView:在 Activity 或 Fragment 中,将 PagingData 设置给 RecyclerView:

class MainActivity : AppCompatActivity() {

    private val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    private val viewModel by lazy {
        ViewModelProvider(this).get(MyViewModel::class.java)
    }

    private val adapter by lazy {
        MyPagingDataAdapter()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        initView()
        addDataObserve()

    }

    private fun addDataObserve() {
        lifecycleScope.launch {
            viewModel.myPager.flow.collectLatest {
                adapter.submitData(it)
            }
        }
    }

    private fun initView() {
       binding.apply {
           list.layoutManager = LinearLayoutManager(this@MainActivity)
           list.adapter = adapter
       }
    }
}

在上述示例中,我们通过 collectLatest 来监听 PagingData 的变化,并在数据更新时使用 adapter.submitData(data) 来提交新的数据。

这样,就完成了使用 Paging3将数据展示在 RecyclerView 中的过程。

总结

本文简单介绍了Paging3的基本概念和使用流程,主要有:

  • PagingData及其作用

  • PagingDataSource的概念和使用

  • Pager的作用和创建过程

  • PagingDataAdapter的创建和使用

可以发现,Paging3上手难度还是比较低的,但是Paging3的内容远远不止于此,让我们继续探索,冲冲冲