手把手教你使用Android Paging Library
當我們用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專案並執行