让你易上手的Jetpack Paging3教程

4,983 阅读4分钟

1. 什么是Paging3

Paging3是谷歌开发的一个针对Android系统的库。Paging3可以让应用无缝的加载并展示从本地或者服务器中获取的数据。

对比Paging2,Paging3有以下5点的更新。

  1. PagingSource的实现更加简单。
  2. 支持Flow
  3. 增加请求数据时状态的回调,
  4. 支持设置HeaderFooter
  5. 支持多种方式请求数据,比如网络请求和数据库请求。

关于Paging2的教程可以看这篇:juejin.cn/post/684490…

2. Paging3的架构

1_H3gQgWFyUvt1K9QKkho3Pg.png

上图所示的是利用Paging3的应用的架构。在Repository层获取数据,然后传给ViewModel层中的Pager,最后PagingDataAdaperViewModel中获取Flow数据并展示。

  • PagingSource: 是一个单一的数据源,PagingSource可以从本地或服务器中加载数据。
  • RemoteMediator: 也是一个单一的数据源,在PagingSource中没有数据的时候,会使用RemoteMediator的数据。如果数据既存在于数据库,又存在于服务器中,更多的情况PagingSource用于数据库请求,RemoteMediator用于服务器请求。
  • PagingData: 单次分页数据的容器。
  • Pager: 用来构建Flow的类,实现数据加载完成的回调。
  • PagingDataAdapter: 分页加载数据的RecyclerView的适配器。

3. Paging3的实现

3.1 引入库

根据项目的需要添加库依赖到当前项目的Gradle文件中。

    def paging_version = "3.0.0-beta01"

    implementation "androidx.paging:paging-runtime:$paging_version"
    
    // alternatively - without Android dependencies for tests
    testImplementation "androidx.paging:paging-common:$paging_version"

    // optional - RxJava2 support
    implementation "androidx.paging:paging-rxjava2:$paging_version"

    // optional - RxJava3 support
    implementation "androidx.paging:paging-rxjava3:$paging_version"

    // optional - Guava ListenableFuture support
    implementation "androidx.paging:paging-guava:$paging_version"

    // Jetpack Compose Integration
    implementation "androidx.paging:paging-compose:1.0.0-alpha08"

3.2 配置DataStore

在Paging2中会有3种类型的Page Source,这导致我们在使用之前需要好好考虑应该使用哪一种。这着实让开发者抓耳挠腮。
但是在Paging3种只有一种Page Source,不用在为选择哪一种而苦恼了,直接闭着眼睛用就可以了。
Page Source需要继承自PagingSource, 同时需要重写loadgetRefreshKey方法。

class PersonDataSource(private val repository: PersonRepository) : PagingSource<Int, Person>() {
    // Load data when got data from DataSource(server or local cache).
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Person> {
        val pos = params.key ?: START_INDEX
        val startIndex = pos * params.loadSize + 1
        val endIndex = (pos + 1) * params.loadSize
        return try {
            // Get data from DataSource
            val personList = repository.getPersonList(startIndex, endIndex)
            // Return data to RecyclerView by LoadResult
            LoadResult.Page(
                personList,
                if (pos <= START_INDEX) null else pos - 1,
                if (personList.isEmpty()) null else pos + 1
            )
        } catch (exception: Exception) {
            // Return exception by LoadResult
            LoadResult.Error(exception)
        }
    }

    // Return the position of data when refresh RecyclerView.
    override fun getRefreshKey(state: PagingState<Int, Person>): Int? {
        return 0
    }

    companion object {
        private const val START_INDEX = 0
    }
}

从上面的代码可以知道,我们可以通过LoadParams参数可以获得Paging的loadSizekey(起始位置)的信息。

3.3 创建一个可观察的数据集

接下来我们需要在ViewModel中创建一个可观察的数据集。在这里所说的可观察数据集指的是LiveData,FlowRxJava中的ObservableFlowable等数据类型。需要值得注意的是,如果使用的RxJava的数据集,不要忘记引入RxJava的库。

在我的Demo中我使用的是Flow

    var personList =
        Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = false,
                initialLoadSize = 20
            ),
            pagingSourceFactory = {
                PersonDataSource(repository)
            }
        ).flow

PagingConfigPagingSource传给Pager, Pager会返回一个Flow
PagingConfig中,我们需要设置PageSize,enablePlaceholdersinitialLoadSize
PagingSourceFactor中传入我们在上面已经写好的DataSource。

3.4 创建一个Adapter

我们需要创建一个RecyclerView的adapter,该adapter需要继承自PagingDataAdapter。具体的写法和ListAdapter类似。

class PersonAdapter(private val context: Context) :
    PagingDataAdapter<Person, PersonAdapter.ViewHolder>(PersonDiffCallback()) {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val person = getItem(position)
        holder.binding.also {
            it.textViewName.text = person?.name
            it.textViewAge.text = person?.age.toString()
            if (position % 2 == 0) {
                Glide.with(context).load(person?.photoUrl).into(it.imageView)
            } else {
                Glide.with(context).load(R.drawable.ic_studio_icon).into(it.imageView)
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder(
            RecyclerItemBinding.inflate(
                LayoutInflater.from(context), parent, false
            )
        )
    }

    class ViewHolder(val binding: RecyclerItemBinding) :
        RecyclerView.ViewHolder(binding.root)
}

class PersonDiffCallback : DiffUtil.ItemCallback<Person>() {
    override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
        return oldItem.name == newItem.name
    }

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

3.5 显示item

这里的实现方法跟一般的RecyclerView中显示Item是一样的。简而言之,需要实现以下几项。

  1. 创建和设置adapter的实例。
  2. 创建coroutines.
  3. 在coroutines scope中接收Flow数据。
        val adapter = PersonAdapter(this)
        binding.recyclerView.adapter = adapter

        // launch an coroutines, and received flow data. Finally, submit data to the adapter.
        lifecycleScope.launch {
            viewModel.personList.collectLatest {
                adapter.submitData(it)
            }
        }

3.6 观察加载数据的状态

我们可以在adapter中设置一个listener来观察加载数据的状态。一共有3种状态。

  1. LoadState.Loading: 应用正在加载数据。
  2. LoadState.NotLoading: 应用没有在加载数据。
  3. LoadState.Error: 应用在加载数据的时候发生了错误。
        adapter.addLoadStateListener {state ->
            when(state.refresh){
                is LoadState.Loading -> {
                    binding.swipeRefreshLayout.isRefreshing = true
                }
                is LoadState.NotLoading -> {
                    binding.swipeRefreshLayout.isRefreshing = false
                }
                is LoadState.Error -> {
                    // show an error dialog.
                }
            }
        }

3.7 设置Header和Footer

我们可以在数据加载中和加载更多时显示特定的View。
首先,我们要创建一个继承自LoadStateAdapter的Adapter,
具体实现方法跟PagingDataAdapter的实现方法相似,不同的点在onBindViewHolder方法上。
关于具体的实现可以参考下面代码。

class PersonLoadStateAdapter(private val context: Context) :
    LoadStateAdapter<PersonLoadStateAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: ViewHolder, loadState: LoadState) {
        // change the view when LoadStat was changed.
        when(loadState){
            is LoadState.Loading -> {
                holder.binding.progressBar.visibility = View.VISIBLE
                holder.binding.errorMsg.visibility = View.INVISIBLE
                holder.binding.retryButton.visibility = View.INVISIBLE
            }
            is LoadState.NotLoading -> {
                holder.binding.progressBar.visibility = View.INVISIBLE
                holder.binding.errorMsg.visibility = View.INVISIBLE
                holder.binding.retryButton.visibility = View.INVISIBLE
            }
            is LoadState.Error -> {
                holder.binding.progressBar.visibility = View.INVISIBLE
                holder.binding.errorMsg.visibility = View.VISIBLE
                holder.binding.retryButton.visibility = View.VISIBLE
            }
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ViewHolder {
        return ViewHolder(
            LoadStateFooterViewItemBinding.inflate(
                LayoutInflater.from(context), parent, false
            )
        )
    }

    class ViewHolder(val binding: LoadStateFooterViewItemBinding) :
        RecyclerView.ViewHolder(binding.root)
}

第二步,我们可以通过withLoadStateHeaderAndFooter方法把LoadStateAdapter拼接到PagingDataAdapter上。然后把拼接完后的Adapter传递给RecyclerView即可。

        val adapter = PersonAdapter(this)

        val concatAdapter: ConcatAdapter = adapter.withLoadStateFooter(
            footer = PersonLoadStateAdapter(this)
        )

        binding.recyclerView.adapter = concatAdapter

4. Others

1_xHmdFg_W2phZRFQfdYHKXA.gif
Github: github.com/HyejeanMOON…

让你易上手的Jetpack Compose教程

1. Compose的编程思想

Jetpack相关教程

1. 让你易上手的Jetpack Paging3教程
2. 让你易上手的Jetpack DataStore教程
3. Android Jetpack Room的详细教程
4. Paging在Android中的应用
5. Android WorkManager的使用

其他教程

1. 神一样的存在,Dagger Hilt !!
2. Android10的分区存储机制(Scoped Storage)适配教程
3. Android的属性动画(Property Animation)详细教程
4. Android ConstraintLayout的易懂教程
5. Google的MergeAdapter的使用