让你易上手的Jetpack Paging3教程

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…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享