Android Jetpack(9):Paging3 的使用

Paging介绍

Paging是Google 2018IO大会最新发布的Jetpack中的一个组件,主要用于大数据的分页加载,这篇文章就来探索一下关于Paging的简单使用。

Paging它是什么,怎么用?

一句话概述: Paging 可以使开发者更轻松在 RecyclerView 中 分页加载数据。

分页效果实现方式

在使用之前,我们需要搞明白的是,目前Android设备中比较主流的两种 分页模式,用我的语言概述,大概是:

  • 传统的 上拉加载更多 分页效果
  • 无限滚动分页效果(当滑动了一定量的数据时,会自动请求加载下一页的数据)

举个例子,传统的 上拉加载更多分页效果,滑到底部,再上拉显示footer,才会加载数据。而无限滚动分页效果,如果我们慢慢滑动,当滑动了一定量的数据(这个阈值一般是数据总数的某个百分比)时,会自动请求加载下一页的数据,如果我们继续滑动,到达一定量的数据时,它会继续加载下一页数据,直到加载完所有数据——在用户看来,就好像是一次就加载出所有数据。

很明显,无限滚动 分页效果带来的用户体验更好,不仅是京东,包括 知乎 等其它APP,所采用的分页加载方式都是 无限滚动 的模式,而 Paging 也正是以无限滚动 的分页模式而设计的库。

特点

  • 每一页的数据会缓存至内存中,以此保证处理分页数据时更有效的使用系统资源
  • 同时多个相同的请求只会触发一个,确保App有效的使用网络资源和系统资源
  • 可以配置RecyclerView的adapters,让其滑动到末尾自动发起请求
  • 支持Kotlin协程、Flow、LiveData以及RxJava
  • 内置错误处理支持,如刷新和重试功能。

逻辑图

在这里插入图片描述

Paging3依赖

    def paging_version = "3.0.0-alpha03"

    implementation "androidx.paging:paging-runtime-ktx:$paging_version"

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

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

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

Paging基本使用

基本使用主要包含如下内容:

  • 配置数据源:PagingSource
  • 构建分页数据:Pager、PagingData
  • 构建RecyclerView
  • Adapter:PagingDataAdapter
  • 展示分页UI列表数据
  • 加载状态
  • 预取阈值
  • 支持RxJava

Paging的使用案例

通过一个案例来学习Paging的使用:请求网络数据,分页加载。

案例使用的接口:

https://www.wanandroid.com/article/list/1/json

ApiService

interface ApiService {

    /**
     * https://www.wanandroid.com/article/list/1/json
     */
    @GET("article/list/{pageNum}/json")
    suspend fun getListData(@Path("pageNum") pageNum: Int): BaseResp<ArticleBean>
}

BaseResp

class BaseResp<T>(val data: T, val errorCode: Int, val errorMsg: String)

ArticleBean

data class ArticleBean(
    val curPage: Int,
    val size: Int,
    val total: Int,
    val pageCount:Int,
    val datas: List<ArticleItemBean>
)

data class ArticleItemBean(
    val title: String,
    val id:Int
)

RetrofitClient

class RetrofitClient {

    // 单例实现
    companion object {
        val instance: RetrofitClient by lazy { RetrofitClient() }
    }

    private val retrofit: Retrofit

    private val interceptor: Interceptor

    init {
        //通用拦截
        interceptor = Interceptor { chain ->
            val request = chain.request()
                .newBuilder()
                .addHeader("Content_Type", "application/json")
                .addHeader("charset", "UTF-8")
                .addHeader("token", "")
                .build()

            chain.proceed(request)
        }

        //Retrofit实例化
        retrofit = Retrofit.Builder()
            .baseUrl("https://www.wanandroid.com/")
            .addConverterFactory(GsonConverterFactory.create())
            //.client(initClient())
            .build()
    }

    /**
     *  OKHttp创建
     */
    private fun initClient(): OkHttpClient? {
        return OkHttpClient.Builder()
            .addInterceptor(interceptor)
            .addInterceptor(initLogInterceptor())
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .build()
    }

    /**
     * 日志拦截器
     */
    private fun initLogInterceptor(): Interceptor? {
        val interceptor = HttpLoggingInterceptor()
        interceptor.level = HttpLoggingInterceptor.Level.BODY
        return interceptor

    }

    /**
     * 具体服务实例化
     */
    fun <T> createApiService(service: Class<T>): T {
        return retrofit.create(service)
    }
}

配置数据源:PagingSource

/**
 * Cerated by xiaoyehai
 * Create date : 2020/11/19 14:36
 * description : 配置数据源数据源,获取数据是通过DataSource实现的。
 */

//PagingSource的两个泛型参数分别是表示当前请求第几页的Int,以及请求的数据类型
class MainDataSource() : PagingSource<Int, ArticleItemBean>() {

	 //实现这个方法来触发异步加载(例如从数据库或网络)。 这是一个suspend挂起函数,可以很方便的使用协程异步加载
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ArticleItemBean> {

        try {
            //param.key为空时,默认加载第1页数据。
            val currentPage = params.key ?: 0

            //获取网络数据
            val response = RetrofitClient.instance.createApiService(ApiService::class.java)
                .getListData(currentPage)

            //请求成功使用LoadResult.Page返回分页数据,prevKey 和 nextKey 分别代表前一页和后一页的索引。
            return LoadResult.Page(
                //加载的数据
                data = response.data.datas,

                //上一页,如果有上一页设置该参数,否则不设置
                prevKey = if (currentPage == 0) null else currentPage - 1,

                //加载下一页的key 如果传null就说明到底了
                nextKey = if (response.data.curPage == response.data.pageCount) null else currentPage + 1
            )
        } catch (e: Exception) {
            //请求失败使用LoadResult.Error返回错误状态
            return LoadResult.Error(e)
        }
    }
}
  • 继承PagingSource,需要两个泛型,第一个表示下一页数据的加载方式,比如使用页码加载可以传Int,使用最后一条数据的某个属性来加载下一页就传别的类型比如String等。
  • 实现其load方法来触发异步加载,可以看到它是一个用suspend修饰的挂起函数,可以很方便的使用协程异步加载。
  • 其参数LoadParams中有一个key值,我们可以拿出来用于加载下一页。
  • 返回值是一个LoadResult,出现异常调用LoadResult.Error(e),正常强开情况下调用LoadResult.Page方法来设置从网络或者数据库获取到的数据。
  • prevKey 和 nextKey 分别代表下次向上加载或者向下加载的时候需要提供的加载因子,比如我们通过page的不断增加来加载每一页的数据,nextKey就可以传入下一页page+1。如果设置为null的话说明没有数据了。

MainRepository:数据仓库层Repository

Repository层主要使用PagingSource这个分页组件来实现,每个PagingSource对象都对应一个数据源,以及该如何从该数据源中查找数据。PagingSource可以从任何单个数据源比如网络或者数据库中查找数据。

Repository层还有另一个分页组件可以使用RemoteMediator,它是一个分层数据源,比如有本地数据库缓存的网络数据源。

class MainRepository {
    fun getArticleData() =
        Pager(PagingConfig(pageSize = 20, prefetchDistance = 10)) { MainDataSource() }.flow
}

代码虽少不过有两个重要的对象:Pager 和 PagingData

  • Pager是进入分页的主要入口,它需要4个参数:PagingConfig、Key、RemoteMediator、PagingSource其中第一个和第四个是必填的。
  • PagingConfig用来配置加载的时候的一些属性,比如多少条算一页,距离底部多远的时候开始加载下一页,初始加载的条数等等。
  • PagingData 用来存储每次分页数据获取的结果 。
  • flow是kotlin的异步数据流,点类似 RxJava 的 Observable。

构建分页数据:ViewModel

Repository最终返回一个异步流包裹的PagingDataFlow<PagingData>,PagingData存储了数据结果,最终可以使用它将数据跟UI界面关联。

class MainViewModel(application: Application) : AndroidViewModel(application) {

    //抽到MainRepository实现
   // val listData = Pager(PagingConfig(pageSize = 20)) { MainDataSource() }.flow.cachedIn(viewModelScope)


    private val repository:MainRepository by lazy { MainRepository() }
    
    /**
     * Pager 分页入口 每个PagingData代表一页数据 
     */
    fun getArticleData() = repository.getArticleData().cachedIn(viewModelScope)

    /**
     * mainListAdapter
     */
    fun getArticleData2() = repository.getArticleData().asLiveData()

}

MainActivity:展示分页列表数据

class MainActivity : AppCompatActivity() {

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

        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)

        val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        binding.rv.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
        val mainListAdapter = MainListAdapter()
        binding.rv.adapter = mainListAdapter

        lifecycleScope.launch {
            viewModel.getArticleData().collectLatest {
                mainListAdapter.submitData(it)
            }
        }

        /*  viewModel.getArticleData2().observe(this, Observer {
              lifecycleScope.launchWhenCreated {
                  mainListAdapter.submitData(it)
              }
          })*/
    }
}

调用adapter的submitData方法来触发页面的渲染。这个方法是一个suspend修饰的挂起方法,所以将它放到一个有生命周期的协程中调用。如果不想放到协程中可以调用另外一个两个参数的方法adapter.submitData(lifecycle,it)传入lifecycle就行了。

MainListAdapter:构建Adapter

Adapter的创建跟Paging2.x写法差不多,不过继承的类不一样了,Paging2.x继承的是PagedListAdapter,在3.0中PagedListAdapter已经没有了,需要继承PagingDataAdapter。

写法跟写正常的RecyclerView.Adapter基本一样,就加了一样东西,需要在构造方法里传入一个DiffUtil.ItemCallback用来确定差量更新的时候的计算规则。

class MainListAdapter : PagingDataAdapter<ArticleItemBean, MainListAdapter.BindingViewHolder>(DataDifferntiator) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder {
        val binding = ItemRvBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return BindingViewHolder(binding)
    }

    override fun onBindViewHolder(holder: BindingViewHolder, position: Int) {
        val binding = holder.binding
        binding.listBean = getItem(position)
        binding.executePendingBindings()
    }

    class BindingViewHolder(val binding: ItemRvBinding) : RecyclerView.ViewHolder(binding.root) 

    object DataDifferntiator : DiffUtil.ItemCallback<ArticleItemBean>() {
        override fun areItemsTheSame(oldItem: ArticleItemBean, newItem: ArticleItemBean): Boolean {
            return oldItem.id== newItem.id
        }

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

    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbarSize="10dp"
            android:scrollbars="vertical"
            tools:listitem="@layout/item_rv"
            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

item_rv.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="listBean"
            type="com.zly.jetpack.bean.ArticleItemBean" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="match_parent"
            android:layout_height="70dp"
            android:gravity="center_vertical"
            android:paddingLeft="10dp"
            android:text="@{listBean.title}"
            android:textColor="@color/black"
            android:textSize="16sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="条目" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

刷新和重试

Paging3.0中调用刷新的方法比Paging2.x中方便多了,直接就提供了刷新的方法,并且还提供了加载数据出错后的重试方法。

在下拉刷新控制的下拉监听中直接调用adapter.refresh()方法就可以完成刷新了,那什么时候关闭刷新动画呢,需要调用adapter.loadStateFlow.collectLatest方法来监听

refreshView.setOnRefreshListener {
         adapter.refresh()
 }
 lifecycleScope.launchWhenCreated {
            @OptIn(ExperimentalCoroutinesApi::class)
            adapter.loadStateFlow.collectLatest {
                if(it.refresh !is LoadState.Loading){
                    refreshView.finishRefresh()
                }
            }
        }

收集流的状态,如果是不是Loading状态的说明加载完成了,可以关闭动画了。

预取阈值

构建Pager时,使用到PagingConfig,其中有一个属性值为prefetchDistance,用于表示距离底部多少条数据开始预加载,设置0则表示滑到底部才加载。默认值为分页大小。若要让用户对加载无感,适当增加预取阈值即可。比如调整到分页大小的5倍。

class MainViewModel(application: Application) : AndroidViewModel(application) {

    val listData =
        Pager(PagingConfig(pageSize = 20,prefetchDistance = 10)) { MainDataSource() }.flow.cachedIn(viewModelScope)

}

加载状态

LoadState分为LoadState.NotLoading、LoadState.Loading、LoadState.Error三种状态。而处理PagingAdapter的加载状态的方式有两种:

  • 使用监听器获取并处理加载状态
  • 使用PagingAdapter处理加载状态

使用监听器

       mainListAdapter.addLoadStateListener { loadState ->
            Toast.makeText(this, "$loadState", Toast.LENGTH_LONG).show()
       }

使用PagingAdapter

官方提供了工具类LoadStateAdapter给开发者处理加载状态的问题,可以添加header和footer。

PagingDataAdapter可以设置头部和底部的加载进度或者加载出错时候的布局,这样当处于加载中的状态的时候,可以显示加载动画,加载出错的时候可以显示出重试的按钮。用起来也简单舒服。

需要自定义一个Adapter继承自LoadStateAdapter,并将这个Adapter设置给最开始adapter就可以了。

class MainLoadStateAdaptr(private val adapter: MainListAdapter):LoadStateAdapter<MainLoadStateAdaptr.BindingViewHolder>() {

    override fun onBindViewHolder(holder: BindingViewHolder, loadState: LoadState) {
        val binding = holder.binding

        when (loadState) {
            is LoadState.Error -> {
                binding.pb.visibility = View.GONE
                binding.tvState.visibility = View.VISIBLE
                binding.tvState.text = "加载失败..."
                binding.tvState.setOnClickListener { adapter.retry()
                }
            }
            is LoadState.Loading -> {
                binding.pb.visibility = View.VISIBLE
                binding.tvState.visibility = View.VISIBLE
                binding.tvState.text = "正在加载..."
            }
            is LoadState.NotLoading -> {
                binding.pb.visibility = View.GONE
                binding.tvState.visibility = View.GONE
            }
        }
    }

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

    class BindingViewHolder(val binding: ItemFooterBinding) : RecyclerView.ViewHolder(binding.root)
}

通过withLoadStateHeaderAndFooter将其添加到MainListAdapter:

       binding.rv.adapter=mainListAdapter.withLoadStateHeaderAndFooter(
            MainLoadStateAdaptr(mainListAdapter), MainLoadStateAdaptr(mainListAdapter)
          )

也可以单独只设置hader或footer:

// only footer
mainListAdapter.withLoadStateFooter(
    HeaderFooterAdaptermainListAdapter()
}

// only header
mainListAdapter.withLoadStateHeader(
   HeaderFooterAdapter(mainListAdapter)
)

Paging3支持RxJava

如果你不习惯使用Coroutine或者Flow,Paging3同样支持RxJava。

添加RxJava的相关依赖:

 implementation "androidx.paging:paging-rxjava2-ktx:$paging_version"
 
 implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'

使用RxPagingSource替代PagingSource:

class MainDataSource() : RxPagingSource<Int, ArticleItemBean>() {
	override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, ArticleItemBean>> {
         TODO("Not yet implemented")
    }

}

ApiService接口也改为Single:

interface ApiService {

    /**
     * https://www.wanandroid.com/article/list/1/json
     */
    @GET("article/list/{pageNum}/json")
    fun getListData(@Path("pageNum") pageNum: Int):Single<BaseResp<ArticleBean>>
}

配置Pager时,使用.observable将LiveData转为RxJava:

class MainViewModel(application: Application) : AndroidViewModel(application) {

    val listData =
        Pager(PagingConfig(pageSize = 20,prefetchDistance = 2)) { MainDataSource() }
            .observable
            .cachedIn(viewModelScope)

}

在Activity中请求数据:

viewModel.listData.subscribe { 
    mainListAdapter.submitData(lifecycle,it)
}

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值