1. 程式人生 > >手把手教你使用Android Paging Library

手把手教你使用Android Paging Library

當我們用RecyclerView來展示伺服器返回的大量資料時,通常我們都需要實現分頁的效果。以前我們都是通過監聽RecyclerView的滾動事件,當RecyclerView滑動到底部的時候再次請求網路,把資料展示到RecyclerView上。現在Google提供了一個分頁庫來幫助開發者更輕鬆的實現在RecyclerView中逐步而且優雅地載入資料

本文我將以Google官方提供的PagingWithNetworkSample為例,手把手教你使用Android分頁庫。官方Demo地址

首先我們來簡單看一下Paging庫的工作示意圖,主要是分為如下幾個步驟

  1. 使用DataSource從伺服器獲取或者從本地資料庫獲取資料(需要自己實現)
  2. 將資料儲存到PageList中(Paging庫已實現)
  3. 將PageList的資料submitList給PageListAdapter(需要自己呼叫)
  4. PageListAdapter在後臺執行緒對比原來的PageList和新的PageList,生成新的PageList(Paging庫已實現對比操作,使用者只需提供DiffUtil.ItemCallback實現)
  5. PageListAdapter通知RecyclerView更新

image

接下來我將使用分頁庫來載入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專案並執行