当我们用RecyclerView来展示服务器返回的大量数据时,通常我们都需要实现分页的效果。以前我们都是通过监听RecyclerView的滚动事件,当RecyclerView滑动到底部的时候再次请求网络,把数据展示到RecyclerView上。现在Google提供了一个分页库来帮助开发者更轻松的实现在RecyclerView中逐步而且优雅地加载数据
本文我将以Google官方提供的PagingWithNetworkSample为例,手把手教你使用Android分页库。官方Demo地址
首先我们来简单看一下Paging库的工作示意图,主要是分为如下几个步骤
- 使用DataSource从服务器获取或者从本地数据库获取数据(需要自己实现)
- 将数据保存到PageList中(Paging库已实现)
- 将PageList的数据submitList给PageListAdapter(需要自己调用)
- PageListAdapter在后台线程对比原来的PageList和新的PageList,生成新的PageList(Paging库已实现对比操作,用户只需提供DiffUtil.ItemCallback实现)
- PageListAdapter通知RecyclerView更新
接下来我将使用分页库来加载https://www.reddit.com(需要翻墙)提供的API数据
1. 创建项目添加依赖
首先,创建一个Android项目,同时勾选Kotlin支持。本项目使用Kotlin编写
然后添加所需要的依赖项
//网络库
implementation "com.squareup.retrofit2:retrofit:2.3.0"
implementation "com.squareup.retrofit2:converter-gson:2.3.0"
implementation "com.squareup.okhttp3:logging-interceptor:3.9.0"
implementation 'androidx.recyclerview:recyclerview:1.0.0'
//Android Lifecycle架构
implementation 'androidx.lifecycle:lifecycle-runtime:2.0.0'
implementation "androidx.lifecycle:lifecycle-extensions:2.0.0"
//Android paging架构
implementation 'androidx.paging:paging-runtime:2.0.0'
2. 定义网络数据请求
我们使用Retrofit来请求网络数据,我们来看下Api定义和实体类定义
API接口
//RedditApi.kt
interface RedditApi {
@GET("/r/{subreddit}/hot.json")
fun getTop(
@Path("subreddit") subreddit: String,
@Query("limit") limit: Int): Call<ListingResponse>
//获取下一页数据,key为after
@GET("/r/{subreddit}/hot.json")
fun getTopAfter(
@Path("subreddit") subreddit: String,
@Query("after") after: String,
@Query("limit") limit: Int): Call<ListingResponse>
class ListingResponse(val data: ListingData)
class ListingData(
val children: List<RedditChildrenResponse>,
val after: String?,
val before: String?
)
data class RedditChildrenResponse(val data: RedditPost)
companion object {
private const val BASE_URL = "https://www.reddit.com/"
fun create(): RedditApi = create(HttpUrl.parse(BASE_URL)!!)
fun create(httpUrl: HttpUrl): RedditApi {
val logger = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger {
Log.d("API", it)
})
logger.level = HttpLoggingInterceptor.Level.BASIC
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.build()
return Retrofit.Builder()
.baseUrl(httpUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(RedditApi::class.java)
}
}
}
实体类
RedditPost.kt
data class RedditPost(
@PrimaryKey
@SerializedName("name")
val name:String,
@SerializedName("title")
val title:String,
@SerializedName("score")
val score:Int,
@SerializedName("author")
val author:String,
@SerializedName("subreddit")
@ColumnInfo(collate = ColumnInfo.NOCASE)
val subreddit:String,
@SerializedName("num_comments")
val num_comments: Int,
@SerializedName("created_utc")
val created: Long,
val thumbnail: String?,
val url: String?
){
var indexInResponse:Int = -1
}
3. 创建DataSource
现在获取网络数据的能力我们已经有了,这和我们之前自己实现分页功能没有什么两样。使用Paging库,第一步我们需要一个DataSource。现在我们需要利用DataSource通过Api去获取数据。DataSource有三个实现类ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource
- ItemKeyedDataSource
列表中加载了N条数据,加载下一页数据时,会以列表中最后一条数据的某个字段为Key查询下一页数 - PageKeyedDataSource 页表中加载了N条数据,每一页数据都会提供下一页数据的关键字Key作为下次查询的依据
- PositionalDataSource 指定位置加载数据,在数据量已知的情况下使用
本例我们将扩展PageKeyedDataSource来加载数据
public abstract void loadInitial(@NonNull LoadInitialParams<Key> params,
@NonNull LoadInitialCallback<Key, Value> callback);
public abstract void loadBefore(@NonNull LoadParams<Key> params,
@NonNull LoadCallback<Key, Value> callback);
public abstract void loadAfter(@NonNull LoadParams<Key> params,
@NonNull LoadCallback<Key, Value> callback);
PageKeyedDataSource中有三个抽象方法。
- loadInitial 表示RecyclerView没有数据第一次请求数据
- loadBefore 请求上一页数据(基本不用)
- loadAfter 请求下一页数据
class PageKeyedSubredditDataSource(
private val redditApi: RedditApi,
private val subredditName: String,
private val retryExecutor: Executor
) : PageKeyedDataSource<String,RedditPost>(){
override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<String, RedditPost>) {
val request = redditApi.getTop(
subreddit = subredditName,
limit = params.requestedLoadSize
)
val response = request.execute()
val data = response.body()?.data
val items = data?.children?.map { it.data } ?: emptyList()
callback.onResult(items, data?.before, data?.after)
}
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<String, RedditPost>) {
redditApi.getTopAfter(subreddit = subredditName,
after = params.key,
limit = params.requestedLoadSize).enqueue(
object : retrofit2.Callback<RedditApi.ListingResponse> {
override fun onFailure(call: Call<RedditApi.ListingResponse>, t: Throwable) {
}
override fun onResponse(
call: Call<RedditApi.ListingResponse>,
response: Response<RedditApi.ListingResponse>) {
if (response.isSuccessful) {
val data = response.body()?.data
val items = data?.children?.map { it.data } ?: emptyList()
callback.onResult(items, data?.after)
} else {
// retry = {
// loadAfter(params, callback)
// }
// networkState.postValue(
// NetworkState.error("error code: ${response.code()}"))
}
}
}
)
}
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<String, RedditPost>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
4. 通过DataSource生成PageList
使用LivePagedListBuilder可以生成LiveData<PageList>对象。有了LiveData当获取到了数据我们就可以通知PageListAdapter去更新RecyclerView了
class InMemoryByPageKeyRepository(private val redditApi: RedditApi,
private val networkExecutor: Executor) : RedditPostRepository {
@MainThread
override fun postOfSubreddit(subReddit: String, pageSize: Int): Listing<RedditPost> {
val sourceFactory = RedditDataSourceFactory(redditApi, subReddit, networkExecutor)
val livePagedList = LivePagedListBuilder(sourceFactory, pageSize)
// provide custom executor for network requests, otherwise it will default to
// Arch Components' IO pool which is also used for disk access
.setFetchExecutor(networkExecutor)
.build()
return Listing(
pagedList = livePagedList
)
}
}
5. PageList submitList到PageAdapter中
PagingActivity.kt
private fun getViewModel(): RedditViewModel {
return ViewModelProviders.of(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val repository = InMemoryByPageKeyRepository(api, Executors.newFixedThreadPool(5))
@Suppress("UNCHECKED_CAST")
return RedditViewModel(repository) as T
}
})[RedditViewModel::class.java]
}
private val api by lazy {
RedditApi.create()
}
private fun initAdapter() {
val adapter = PostsAdapter()
list.adapter = adapter
list.layoutManager = LinearLayoutManager(this)
//Live<PageList<RedditPost>> 增加监听
model.posts.observe(this, Observer<PagedList<RedditPost>> {
adapter.submitList(it)
})
}
6. 创建PageListAdapter的实现类PostsAdapter
class PostsAdapter :PagedListAdapter<RedditPost,RecyclerView.ViewHolder>(object : DiffUtil.ItemCallback<RedditPost>() {
override fun areContentsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean =
oldItem == newItem
override fun areItemsTheSame(oldItem: RedditPost, newItem: RedditPost): Boolean =
oldItem.name == newItem.name}){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return RedditPostViewHolder.create(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if(holder is RedditPostViewHolder) holder.bind(getItem(position))
}
}
7. 创建ViewHodler
这与我们平时创建没有什么两样 略过不表。
8. 完整项目
至此我们就已经完整地将Paging库的关键技术点都已经介绍了。实践出真知。请clone项目并运行