Jetpack Paging 为Android提供了列表分页加载的解决方案,最近发布了最新的3.0-alpha版本。
Paging3 基于Kotlin协程进行了重写,并兼容Flow、RxJava、LiveData等多种形式的API。
本文将通过一个api请求的例子,了解一下Paging3的基本使用。<br/>我们使用https://reqres.in/
提供的mock接口:
request | https://reqres.in/api/users?page=1 |
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
返回分页数据,prevKey
和 nextKey
分别代表前一页和后一页的索引。
请求失败使用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转为FlowcachedIn
表示将结果缓存到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的支持。<br/>虽然目前还是alpha版,但是为了API的变化应该不大,想尝鲜的同学尽快将项目中的Paging升级到最新版本吧~