一个例子让你学会使用Paging3

2,600 阅读4分钟

在这里插入图片描述


Jetpack Paging 为Android提供了列表分页加载的解决方案,最近发布了最新的3.0-alpha版本。 Paging3 基于Kotlin协程进行了重写,并兼容Flow、RxJava、LiveData等多种形式的API。

本文将通过一个api请求的例子,了解一下Paging3的基本使用。
我们使用https://reqres.in/提供的mock接口:

requestreqres.in/api/users?p…
response在这里插入图片描述

Sample代码结构如下:

在这里插入图片描述

Step 01. Gradle dependencies


首先添加paging3的gradle依赖

implementation "androidx.paging:paging-runtime:{latest-version}"

sample中借助retrofit和moshi进行数据请求和反序列化

implementation "com.squareup.retrofit2:retrofit:{latest-version}"
implementation "com.squareup.retrofit2:converter-moshi:{latest-version}"
implementation "com.squareup.moshi:moshi-kotlin:{latest-version}" 

使用ViewModel实现MVVM

implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:{latest-version}"

Step 02. ApiService & Retrifit


定义APIService以及Retrofit实例:

interface APIService {

    @GET("api/users")
    suspend fun getListData(@Query("page") pageNumber: Int): Response<ApiResponse>

    companion object {
        
        private val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        fun getApiService() = Retrofit.Builder()
            .baseUrl("https://reqres.in/")
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
            .create(APIService::class.java)
    }
}

定义挂起函数getListData可以在协程中异步进行分页请求;使用moshi作为反序列化工具

Step 03. Data Structure


根据API的返回结果创建JSON IDL:

{
    "page": 1,
    "per_page": 6,
    "total": 12,
    "total_pages": 2,
    "data": [
    {
        "id": 1,
        "email": "george.bluth@reqres.in",
        "first_name": "George",
        "last_name": "Bluth",
        "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/calebogden/128.jpg"
    },
    {
        "id": 2,
        "email": "janet.weaver@reqres.in",
        "first_name": "Janet",
        "last_name": "Weaver",
        "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg"
    }
    ],
    "ad": {
    "company": "StatusCode Weekly",
    "url": "http://statuscode.org/",
    "text": "A weekly newsletter focusing on software development, infrastructure, the server, performance, and the stack end of things."
}}

根据JSON定义data class

data class ApiResponse(
    @Json(name = "ad")
    val ad: Ad,
    @Json(name = "data")
    val myData: List<Data>,
    @Json(name = "page")
    val page: Int,
    @Json(name = "per_page")
    val perPage: Int,
    @Json(name = "total")
    val total: Int,
    @Json(name = "total_pages")
    val totalPages: Int
)

data class Ad(
    @Json(name = "company")
    val company: String,
    @Json(name = "text")
    val text: String,
    @Json(name = "url")
    val url: String
)

data class Data(
    @Json(name = "avatar")
    val avatar: String,
    @Json(name = "email")
    val email: String,
    @Json(name = "first_name")
    val firstName: String,
    @Json(name = "id")
    val id: Int,
    @Json(name = "last_name")
    val lastName: String
)

Step 04. PagingSource


实现PagingSource接口,重写suspend方法,通过APIService进行API请求

class PostDataSource(private val apiService: APIService) : PagingSource<Int, Data>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
       
    }

}

PagingSource的两个泛型参数分别是表示当前请求第几页的Int,以及请求的数据类型Data

suspend函数load的具体实现:

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> {
    try {
        val currentLoadingPageKey = params.key ?: 1
        val response = apiService.getListData(currentLoadingPageKey)
        val responseData = mutableListOf<Data>()
        val data = response.body()?.myData ?: emptyList()
        responseData.addAll(data)

        val prevKey = if (currentLoadingPageKey == 1) null else currentLoadingPageKey - 1

        return LoadResult.Page(
            data = responseData,
            prevKey = prevKey,
            nextKey = currentLoadingPageKey.plus(1)
        )
    } catch (e: Exception) {
        return LoadResult.Error(e)
    }
}

param.key为空时,默认加载第1页数据。 请求成功使用LoadResult.Page返回分页数据,prevKeynextKey 分别代表前一页和后一页的索引。 请求失败使用LoadResult.Error返回错误状态

Step 05. ViewModel


定义ViewModel,并在ViewModel中创建Pager实例

class MainViewModel(private val apiService: APIService) : ViewModel() {
	val listData = Pager(PagingConfig(pageSize = 6)) {
   	 	PostDataSource(apiService)
	}.flow.cachedIn(viewModelScope)
}
  • PagingConfig用来对Pager进行配置,pageSize表示每页加载Item的数量,这个size一般推荐要超出一屏显示的item数量。
  • .flow表示结果由LiveData转为Flow
  • cachedIn表示将结果缓存到viewModelScope,在ViewModel的onClear之前将一直存在

Step 06. Activity


定义Activity,并创建ViewModel

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
class MainActivity : AppCompatActivity() {

    lateinit var viewModel: MainViewModel
    lateinit var mainListAdapter: MainListAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setupViewModel()
        setupList()
        setupView()
    }

    private fun setupView() {
        lifecycleScope.launch {
            viewModel.listData.collect {
                mainListAdapter.submitData(it)
            }
        }
    }

    private fun setupList() {
        mainListAdapter = MainListAdapter()
        recyclerView.apply {
            layoutManager = LinearLayoutManager(this)
            adapter = mainListAdapter
        }
    }

    private fun setupViewModel() {
        viewModel =
            ViewModelProvider(
                this,
             MainViewModelFactory(APIService.getApiService())
            )[MainViewModel::class.java]
    }
}

ViewModel中需要传入ApiService,所以需要自定义ViewModelFactory

class MainViewModelFactory(private val apiService: APIService) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(apiService) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

setupList中创建RecyclerView.Adapter, setupView通过viewModel.listData加载数据后提交RecyclerView.Adapter显示。

Step 07. PagingDataAdapter


更新MainListAdapter,让其继承PagingDataAdapter,PagingDataAdapter将Data与ViewHolder进行绑定

class MainListAdapter : PagingDataAdapter<Data, MainListAdapter.ViewHolder>(DataDifferntiator) {

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view)

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.itemView.textViewName.text =
            "${getItem(position)?.firstName} ${getItem(position)?.lastName}"
        holder.itemView.textViewEmail.text = getItem(position)?.email

    }


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

    object DataDifferntiator : DiffUtil.ItemCallback<Data>() {

        override fun areItemsTheSame(oldItem: Data, newItem: Data): Boolean {
            return oldItem.id == newItem.id
        }

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

}

Paging接收一个DiffUtil的Callback处理Item的diff,这里定一个DataDifferntiator,用于PagingDataAdapter的构造。

ViewHolder的layout定义如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="4dp">

    <TextView
        android:id="@+id/textViewName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="4dp"
        android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large" />

    <TextView
        android:id="@+id/textViewEmail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:layout_marginBottom="4dp" />
</LinearLayout>

至此,我们已经可以分页加载的数据显示在Activity中了,接下来会进一步优化显示效果。

Step 08. LoadingState


列表加载时,经常需要显示loading状态,可以通过addLoadStateListener实现

mainListAdapter.addLoadStateListener {
    if (it.refresh == LoadState.Loading) {
        // show progress view
    } else {
        //hide progress view
    }
}

Step 09. Header & Footer


创建HeaderFooterAdapter继承自LoadStateAdapter,onBindViewHolder中可以返回LoadState

class HeaderFooterAdapter() : LoadStateAdapter<HeaderFooterAdapter.ViewHolder>() {

    override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {

        if (loadState == LoadState.Loading) {
            //show progress viewe
        } else //hide the view
       
    }

    override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
        return LoadStateViewHolder(
           //layout file
        )
    }

    class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view)
}

通过withLoadStateHeaderAndFooter将其添加到MainListAdapter

mainListAdapter.withLoadStateHeaderAndFooter(
    header = HeaderFooterAdapter(),
    footer = HeaderFooterAdapter()
)

也可以单独只设置hader或footer

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

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

Others:RxJava


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

添加RxJava的相关依赖

implementation "androidx.paging:paging-rxjava2:{latest-version}"
implementation "com.squareup.retrofit2:adapter-rxjava2:{latest-version}"

使用RxPagingSource替代PagingSource

class PostDataSource(private val apiService: APIService) : RxPagingSource<Int, Data>() {

}

load改为返回Single接口的普通方法

override fun loadSingle(params: LoadParams<Int>): Single<LoadResult<Int, Data>> {

}

ApiService接口也改为Single

interface APIService {

    @GET("api/users")
     fun getListData(@Query("page") pageNumber: Int): Single<ApiResponse>

    companion object {

        private val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        fun getApiService() = Retrofit.Builder()
            .baseUrl("https://reqres.in/")
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
            .create(APIService::class.java)
    }
}

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

val listData = Pager(PagingConfig(pageSize = 6)) {
    PostDataSource(apiService)
}.observable.cachedIn(viewModelScope)

在Activity中请求数据

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

Conclusion


Paging3基于Coroutine进行了重写,推荐使用Flow作为首选进行数据请求,当然它也保留了对RxJava的支持。
虽然目前还是alpha版,但是为了API的变化应该不大,想尝鲜的同学尽快将项目中的Paging升级到最新版本吧~