手把手教你使用Android Paging Library

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/future234/article/details/83036188

当我们用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项目并运行

猜你喜欢

转载自blog.csdn.net/future234/article/details/83036188