Android—Jetpack-Paging组件详解

前言

在前几篇中,主要讲解了Jetpack其他组件相关,在本篇中,将会对Paging组件进行讲解。

1、认识Jetpack—Paging

1.1 Paging组件的意义

分页加载是在应用程序开发过程中十分常见的需求,Paging就是Google为了方便Android开发者完成分页加载而设计的一个组件,它为几种常见的分页机制提供了统一的解决方案,让我们可以吧更多的经历专注在业务代码上。

1.2 Paging支持的架构类型

在这里插入图片描述
如图所示

Paging支持的架构类型:

  • 网络数据

    • 对网络数据进行分页加载,是最常见的一种分页需求,也是我们学习的重点。不同的公司针对分页机制所设计的API接口通常也不太一样,但总体而言可以归纳为3种。
    • Paging组件提供了3种不同的方案,以应对不同的分页机制。
    • 分别是:PositionDataSource、PageKeyedDataSource、ItemKeyedDataSource
  • 数据库

    • 掌握了网络数据分页之后,数据库数据分页将会容易很多,无非就是数据源的替换
  • 网络+数据库

    • 出于用户体验的考虑,我们通常会对网络数据进行缓存,以便用户在下次打开应用程序时,应用程序可以先展示缓存数据。
    • 我们通常会利用数据库对网络数据进行缓存,这意味着我们需要同时处理好网络和数据库这两个数据源。
    • 多数据源会让业务逻辑变得更为复杂,所以!我们通常采用单一数据源作为解决方案。
    • 既从网络获取的数据,直接缓存进数据库,列表只从数据库这个唯一的数据源获取数据,这里我们会学习到BoundaryCallback

1.4 Paging的工作原理

在这里插入图片描述

如图所示

Paging的3个核心类

  • PagedListAdpater

    • RecycleView 需要搭配适配器使用,如果希望使用Paging组件,适配器需要继承自PagedListAdpater
  • PagedList

    • PagedList负责通知DataSource何时获取数据,以及如何获取数据。例如,何时加载第一页/下一页,第一页加载的数量、提前多少条数据开始执行预加载等,从DataSource获取的数据将存储在PagedList中
  • DataSource

    • 在DataSource中执行具体的数据载入工作,数据可以来自网络,也可以来自本地数据库,根据分页机制的不同,Paging为我们提供了3种DataSource
    • 数据的载入需要在工作线程中进行

概念终于说完了,阔以开始实战了!

2、准备工作

2.1 基础配置

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
}

...def retrofit_version = '2.9.0'
    implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
    implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
    implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
    implementation 'androidx.paging:paging-runtime-ktx:2.1.2'
    implementation 'com.squareup.picasso:picasso:2.71828'
...

注意:这里我们使用的paging:paging版本为:2.1.2,在下一篇Flow+Paging文章中,将会使用3+版本。

2.2 RetrofitClient

class RetrofitClient {

    companion object {
        private const val BASE_URL = "http://192.168.0.104:8080/pagingserver/"
        private var mInstance: RetrofitClient? = null
        @Synchronized
        @JvmStatic
        fun getInstance(): RetrofitClient {
            if (mInstance == null) {
                mInstance = RetrofitClient()
            }
            return mInstance as RetrofitClient
        }
    }
    private var retrofit: Retrofit? = null
    constructor() {
        retrofit = Retrofit.Builder().baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(getClient())
            .build()
    }
    private fun getClient(): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
    fun getApi(): Api {
        return retrofit!!.create(Api::class.java)
    }
}

OK,准备工作做好了,上面不是提到过三个核心类,那么就按照上面图解顺序依次讲解:

3、Paging的三个核心类

3.1 PagedListAdpater

//分析点1
class MoviePagedListAdapter : PagedListAdapter<Movie, MovieViewHolder> {


    //<html>None of the following functions can be called with the arguments supplied:<br/>protected/*protected and package*/ constructor PagedListAdapter&lt;T : Any!, VH : RecyclerView.ViewHolder!&gt;(config: AsyncDifferConfig&lt;Movie!&gt;) defined in androidx.paging.PagedListAdapter<br/>protected/*protected and package*/ constructor PagedListAdapter&lt;T : Any!, VH : RecyclerView.ViewHolder!&gt;(diffCallback: DiffUtil.ItemCallback&lt;Movie!&gt;) defined in androidx.paging.PagedListAdapter

    private var context: Context? = null

	//分析点2
    constructor(context: Context) : super(DIFF_CALLBACK) {
        this.context = context
    }

	
    companion object {
    	//分析点3
        //差分,只更新需要更新的元素,而不是整个刷新数据源
        private val DIFF_CALLBACK: DiffUtil.ItemCallback<Movie> =
            object : DiffUtil.ItemCallback<Movie>() {
                
                //调用以检查两个对象是否代表同一个项目。
                //如果两个项目代表相同的对象,则为真,如果它们不同,则为假。
                override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean {
                    return oldItem === newItem
                }
				
				//调用以检查两个项目是否具有相同的数据。
				//此信息用于检测项目的内容是否已更改。
				//如果项目的内容相同则为真,如果不同则为假。
                override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean {
                    return oldItem == newItem
                }
            }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
        val root = LayoutInflater.from(context).inflate(R.layout.item, parent, false)
        return MovieViewHolder(root)
    }

    override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
    	//单个实体类可直接通过getItem 获取
        val movie = getItem(position)
        if (movie != null) {
        	//使用
            Picasso.get()
                .load(movie.cover)
                .placeholder(R.drawable.ic_launcher_foreground)
                .error(R.drawable.ic_launcher_foreground)
                .into(holder.imageView)
            if (movie.title!!.length >= 8) {
                movie.title = movie.title!!.substring(0, 7)
            }
            holder.textViewTitle!!.text = movie.title
            holder.textViewRate!!.text = movie.rate
        }
    }

    class MovieViewHolder : RecyclerView.ViewHolder {
        var imageView: ImageView? = null
        var textViewTitle: TextView? = null
        var textViewRate: TextView? = null

        constructor(itemView: View) : super(itemView) {
            imageView = itemView.findViewById(R.id.imageView)
            textViewTitle = itemView.findViewById(R.id.textViewTitle)
            textViewRate = itemView.findViewById(R.id.textViewRate)
        }
    }

}

代码解析:

  • 分析点1:这里我们可以看到对应RecyclerView绑定的Adapter并非直接继承 RecyclerView.Adapter ,而是PagedListAdapter<xxx, xxxHolder>
  • 分析点2:当然使用PagedListAdapter,就需要调用父类构造方法并传对应参数DIFF_CALLBACK,而对应的参数定义在分析点3位置
  • 分析点3:我们可以看到,该DIFF_CALLBACK属性,继承了DiffUtil.ItemCallback<Movie>抽象类,并且实现了对应的抽象方法。里面那个方法就是差分数据比较,也就是说,当有数据发生改变时,UI只会刷新数据改变部分,而非全部!

这个Adpater代码就这块,这里写的比较全,下面所有案例都同用这一个。OK,现在看第二个核心类!

这里用到Movie实体类,

class Movie {

    var id = 0
    var title: String? = null
    var rate: String? = null
    var cover: String? = null
}

3.2 PagedList

class MovieViewModel : ViewModel {

    var moviePagedList: LiveData<PagedList<Movie>>? = null

    constructor() : super() {
        val config = PagedList.Config.Builder() //设置控件占位
        	//使用此配置传递 false 以禁用 PagedLists 中的空占位符。 <p> 如果未设置,则默认为 true。
            .setEnablePlaceholders(false)
            .setPageSize(MovieDataSource.PER_PAGE) //设置当距离底部还有多少条数据时开始加载下一页
            .setPrefetchDistance(2) //设置首次加载的数量
            .setInitialLoadSizeHint(MovieDataSource.PER_PAGE * 2)
            .setMaxSize(65536 * MovieDataSource.PER_PAGE)
            .build()
            
        moviePagedList = LivePagedListBuilder(MovieDataSourceFactory(), config).build()

    }

}

通过这段代码我们可知:

  • LiveData<PagedList<Movie>> 这里使用了LiveData,由此我们可知,UI刷新的主要责任就在于moviePagedList变量,而该变量使用了PagedList
  • moviePagedList =LivePagedListBuilder(MovieDataSourceFactory(), config).build() 这里通过LivePagedListBuilder.build() 给对应的LiveData数据赋值,第二个参数config就是上面所配置的内容
  • 这里第一个参数用到了MovieDataSourceFactory,因此进入MovieDataSourceFactory看看!

MovieDataSourceFactory

class MovieDataSourceFactory : DataSource.Factory<Int, Movie>() {
    override fun create(): DataSource<Int, Movie> {
        return MovieDataSource()
    }
}

这里我们可以看到:

  • MovieDataSourceFactory 继承了DataSource.Factory<Int, Movie>()
  • 然后在重写的create(),返回了MovieDataSource实例

因此进入第三个核心类,DataSource 看看!

3.3 DataSource


class MovieDataSource : PositionalDataSource<Movie>() {

    companion object {
        const val PER_PAGE = 8
    }
    
    /**
     * 加载初始列表数据
     */
    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Movie>) {

    }

    /**
     * 调用以从 DataSource 加载一系列数据(加载下一页数据)
     */
    override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Movie>) {

    }

}

我们看到

  • 这里的MovieDataSource继承了PositionalDataSource
  • 重写了loadInitial方法,该方法表示:加载初始列表数据时调用
  • 重写了loadRange方法,该方法表示:加载下一页数据时调用

这里的DataSource继承了PositionalDataSource,那么只能继承PositionalDataSource么?如果说有其他的方式,那么对应作用是什么呢?

4、DataSource三种继承方式

不同的继承方式,可实现不同Json的分页效果!

我们先以已经继承过的PositionalDataSource开始讲解

4.1 继承PositionalDataSource

那么 继承PositionalDataSource有什么特点呢?
在这里插入图片描述
如图所示

适用于可通过任意位置加载数据,且目标数据源数量固定的情况。例如,请求时携带参数:start=2&count=5,则表示向服务器请求从第2条数据开始往后延5个数据~

那么!

4.1.1 先来看看服务端的代码
public class PositionalDataSourceServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("PositionalDataSourceServlet doGet");
        int start = Integer.parseInt(request.getParameter("start"));
        int count = Integer.parseInt(request.getParameter("count"));
        System.out.println("start:"+start+",count:"+count);

        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("count",count);
        jsonObject.addProperty("start",start);
        jsonObject.addProperty("total",ServerStartupListener.MOVIES.size());

        Gson gson = new Gson();
        //从MOVIES集合中取出一段数据出来
        List<Movie> searchList = new ArrayList<>();
        for (int i = start; i < start + count; i++) {
            try{
                searchList.add(ServerStartupListener.MOVIES.get(i));
            }catch (IndexOutOfBoundsException e){
                //索引越界,跳出循环
                System.out.println(e.getMessage());
                break;
            }
        }

        JsonArray jsonArray = (JsonArray) gson.toJsonTree(searchList, new TypeToken<List<Movie>>(){}.getType());
        jsonObject.add("subjects",jsonArray);

        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();

        out.print(jsonObject.toString());
        out.close();
    }
}

很原始的写法,不过我们从中可以看到对应的数据结构为:

{
    "count":8,
    "start":0,
    "total":100,
    "subjects":[
        {
            "id":35076714,
            "title":"扎克·施奈德版正义联盟",
            "cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
            "rate":"8.9"
        }
    ]
}

因此

4.1.2 客户端解析json实体类
class Movies {

    //当前返回的数量
    var count = 0

    //起始位置
    var start = 0

    //一共有多少条
    var total = 0

    @SerializedName("subjects")
    var movieList: List<Movie>? = null

    override fun toString(): String {
        return "Movies{" +
                "count=" + count +
                ", start=" + start +
                ", total=" + total +
                ", movieList=" + movieList +
                '}'
    }
}

现在实体类写好了,那么来看看最终MovieDataSource 长啥样

4.1.3 MovieDataSource

class MovieDataSource : PositionalDataSource<Movie>() {

    companion object {
        const val PER_PAGE = 8
    }


    /**
     * 加载初始列表数据
     */
    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Movie>) {
        val startPosition = 0

        RetrofitClient.getInstance()
            .getApi()
            //startPosition 第一个;PER_PAGE 一页加载多少个
            .getMovies(startPosition, PER_PAGE)
            .enqueue(object : Callback<Movies> {

                override fun onResponse(call: Call<Movies?>, response: Response<Movies?>) {
                    if (response.body() != null) {
                        //把数据传递给PagedList
                        callback.onResult(
                            response.body()!!.movieList!!,
                            response.body()!!.start,
                            response.body()!!.total
                        )
                        Log.d("hqk", "loadInitial:" + response.body()!!.movieList)
                    }
                }

                override fun onFailure(call: Call<Movies?>, t: Throwable) {}
            })
    }


    override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Movie>) {
        RetrofitClient.getInstance()
            .getApi()
            //params.startPosition 上一页最后一个,也就是下一页第一个;PER_PAGE 一页加载多少个
            .getMovies(params.startPosition, PER_PAGE)
            .enqueue(object : Callback<Movies?> {
                override fun onResponse(call: Call<Movies?>, response: Response<Movies?>) {
                    if (response.body() != null) {
                        //把数据传递给PagedList
                        callback.onResult(response.body()!!.movieList!!)
                        Log.d("hqk", "loadRange:" + response.body()!!.movieList)
                    }
                }

                override fun onFailure(call: Call<Movies?>, t: Throwable) {}
            })
    }

}

代码解析:

  • loadInitial与loadRange 这两个方法在上面讲过,分别是首次加载,分页加载调用的方法,通过callback.onResult返回当前请求的网络数据

  • callback.onResult对应的callback,就是对应方法的LoadInitialCallback与LoadRangeCallback

  • 而对应调用了getApi().getMovies方法,那么来看看这方法:

    interface Api {
        @GET("pds.do")
        fun getMovies(
            @Query("start") since: Int,
            @Query("count") perPage: Int
        ): Call<Movies>
    }
    
  • 这里可以看到使用了start与count,他们分别代表对应页数开始的下标以及每页的个数

  • 而在loadRange方法里,对应params.startPosition表示,上一页最后一个下标以及当前页面开始下标

万事俱备,只差UI使用!

4.1.4 对应UI使用
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)

        val adapter = MoviePagedListAdapter(this)
        recyclerView.adapter = adapter

        val movieViewModel = MovieViewModel()

        movieViewModel.moviePagedList!!.observe(this, Observer<PagedList<Movie>> {
            adapter.submitList(it)
        })
    }
}

这里我们看到

  • 使用了adapter.submitList(it)代码,而这个方法是PagedListAdapter里面所提供的
  • 它的作用:就是与对应PagedListAdapter里面的差分数据相互配合,然后显示对应Adapter的数据

来看看使用效果

在这里插入图片描述
对应后台打印

PositionalDataSourceServlet doGet
start:0,count:8
PositionalDataSourceServlet doGet
start:8,count:8

综上所述,当实例化MovieViewModel时,将会通过Paging三大核心类进入到对应DataSource,先是启用对应loadInitial方法来加载初始页,当加载下一页时,将会调用loadRange方法,加载下一页数据,然后修改对应LiveData数据,因此通过observe通知UI刷新数据!

接下来就是第二种继承方式!

4.2 继承PageKeyedDataSource

那么继承PageKeyedDataSource有什么作用呢??

在这里插入图片描述
如图所示

该方式适用于数据源以页的方式进行请求的情况。例如,若请求时携带的参数为page=2&pageSize=5,则表示数据源以5条数据为一页,当前返回第二页的5条数据。

那么!

4.2.1 先来看看服务端的代码
public class PageKeyedDataSourceServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("PageKeyedDataSourceServlet doGet");
        int page = Integer.parseInt(request.getParameter("page"));
        int pagesize = Integer.parseInt(request.getParameter("pagesize"));
        System.out.println("page:"+page+",pagesize:"+pagesize);

        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("has_more",true);
        Gson gson = new Gson();
        //从MOVIES集合中取出一段数据出来
        List<Movie> searchList = new ArrayList<>();
        int end = page * pagesize;
        int begin = end - pagesize;
        System.out.println("begin:"+begin+",end:"+end);
        for (int i = begin; i < end; i++) {
            try{
                searchList.add(ServerStartupListener.MOVIES.get(i));
            }catch (IndexOutOfBoundsException e){
                //索引越界,跳出循环
                System.out.println(e.getMessage());
                if (i > ServerStartupListener.MOVIES.size()) {
                    jsonObject.addProperty("has_more",false);
                }
                break;
            }
        }
        JsonArray jsonArray = (JsonArray) gson.toJsonTree(searchList, new TypeToken<List<Movie>>(){}.getType());
        jsonObject.add("subjects",jsonArray);
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.print(jsonObject.toString());
        out.close();
    }
}

通过这段代码可知,对应Json格式为:

{
    "has_more":true,
    "subjects":[
        {
            "id":35076714,
            "title":"扎克·施奈德版正义联盟",
            "cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
            "rate":"8.9"
        }
    ]
}

因此!

4.2.2 客服端解析json实体类
class Movies {

    @SerializedName("has_more")
    var hasMore = false

    @SerializedName("subjects")
    var movieList: List<Movie>? = null


    override fun toString(): String {
        return "Movies{" +
                "hasMore=" + hasMore +
                ", movieList=" + movieList +
                '}'
    }
}

现在实体类写好了,那么来看看最终MovieDataSource 长啥样

4.2.3 MovieDataSource

class MovieDataSource : PageKeyedDataSource<Int, Movie>() {

    companion object {
        const val PER_PAGE = 8
        const val FIRST_PAGE = 1
    }

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, Movie>) {
        
        RetrofitClient.getInstance()
            .getApi()
            .getMovies(
                FIRST_PAGE,
                PER_PAGE
            ).enqueue(object : Callback<Movies> {
                override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
                    if (response.body() != null) {
                        //把数据传递给PagedList
                        callback.onResult(
                            response.body()!!.movieList!!,
                            null,
                            MovieDataSource.FIRST_PAGE + 1
                        )
                        Log.d("hqk", "loadInitial:" + response.body()!!.movieList)
                    }
                }

                override fun onFailure(call: Call<Movies?>, t: Throwable) {}
            })
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
        Log.d("hqk", "loadInitial        loadBefore")
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
        RetrofitClient.getInstance()
            .getApi()
            .getMovies(params.key, PER_PAGE)
            .enqueue(object : Callback<Movies?> {
                override fun onResponse(call: Call<Movies?>, response: Response<Movies?>) {
                    if (response.body() != null) {
                        //把数据传递给PagedList
                        val nextKey = if (response.body()!!.hasMore) params.key + 1 else null
                        callback.onResult(response.body()!!.movieList!!, nextKey)
                        Log.d("hqk", "loadInitial        loadAfter :" + response.body()!!.movieList)
                    }
                }

                override fun onFailure(call: Call<Movies?>, t: Throwable) {}
            })
    }


}

代码解析:

  • loadInitial与loadAfter 这两个方法分别是首次加载,分页加载调用的方法,通过callback.onResult返回当前请求的网络数据

  • callback.onResult对应的callback,就是对应方法的LoadInitialCallback与LoadCallback

  • 而对应调用了getApi().getMovies方法,那么来看看这方法:

    interface Api {
    
    
    //    @GET("pds.do")
    //    fun getMovies(
    //        @Query("start") since: Int,
    //        @Query("count") perPage: Int
    //    ): Call<Movies>
    
        @GET("pkds.do")
        fun getMovies(
            @Query("page") page: Int,
            @Query("pagesize") pagesize: Int
        ): Call<Movies>
    }
    
  • 这里可以看到使用了page与pagesize,他们分别代表对应页数以及每页的个数

  • loadInitial方法里,当首页加载完成调用callback.onResult时,传入了MovieDataSource.FIRST_PAGE + 1这个表示下次直接从第二页开始加载数据

  • 而在loadAfter方法里,通过response.body()!!.hasMore表示是否有下一页,由此来决定下一页是否+1

万事俱备,只差UI使用!

因为UI使用代码与运行效果一样,因此这里就不再次贴对应代码以及对应运行效果了!

直接看后台打印日志

PageKeyedDataSourceServlet doGet
page:1,pagesize:8
begin:0,end:8
PageKeyedDataSourceServlet doGet
page:2,pagesize:8
begin:8,end:16
PageKeyedDataSourceServlet doGet
page:3,pagesize:8
begin:16,end:24

OK,感觉越来越轻松了!趁热打铁,来看看最后一种使用!

4.3 继承ItemKeyedDataSource

还是老规矩,先来看对应特点!
在这里插入图片描述
如图所示

当前方式适用于:当目标数据的下一页需要依赖于上一页数据中最后一个对象中的某个字段作为key的情况,此类分页形式常见于评论功能的实现,例如,若上一页数据中最后一个对象的key为9001,那么在请求下一页时,需要鞋带参数since=9001&pageSize=5,则服务器会返回key=9001之后的5条数据

那么!

4.3.1 先来看看服务端的代码
public class ItemKeyedDataSourceServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("ItemKeyedDataSourceServlet doGet");
        int since = Integer.parseInt(request.getParameter("since"));
        int pagesize = Integer.parseInt(request.getParameter("pagesize"));
        System.out.println("since:"+since+",pagesize:"+pagesize);

        //从MOVIES集合中取出一段数据出来
        List<Movie> searchList = new ArrayList<>();
        //第一次请求,since等于0,重新给since赋值为第一个元素的id
        if(since == 0){
            Movie movie = ServerStartupListener.MOVIES.get(0);
            searchList.add(movie);
            since = movie.getId();
        }

        Gson gson = new Gson();
        for (int i = 0; i < ServerStartupListener.MOVIES.size(); i++) {
            try{
                //通过请求参数since,找到id等于since的元素
                //往后找到pagesize个元素
                Movie movie = ServerStartupListener.MOVIES.get(i);
                if(movie.getId() == since){
                    for (int j = i+1; j < i + pagesize; j++) {
                        searchList.add(ServerStartupListener.MOVIES.get(j));
                    }
                }
            }catch (IndexOutOfBoundsException e){
                //索引越界,跳出循环
                System.out.println(e.getMessage());
                break;
            }
        }
        JsonArray jsonArray = (JsonArray) gson.toJsonTree(searchList, new TypeToken<List<Movie>>(){}.getType());
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.print(jsonArray.toString());
        out.close();
    }
}

通过这段代码可知,对应Json格式为:

[
    {
        "id":35076714,
        "title":"扎克·施奈德版正义联盟",
        "cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp",
        "rate":"8.9"
    }
]

因此使用Movie实体类就可以了,就在外层套上一个list完全满足!

那么就可以直接看对应的DataSource

4.3.2 MovieDataSource

class MovieDataSource : ItemKeyedDataSource<Int, Movie>() {

    companion object {
        const val PER_PAGE = 8
        const val FIRST_PAGE = 1
    }

    override fun getKey(movie: Movie): Int {
        return movie.id
    }

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Movie>) {
        val since = 0
        RetrofitClient.getInstance()
            .getApi()
            .getMovies(since, PER_PAGE)

            .enqueue(object : Callback<List<Movie>> {
                override fun onResponse(
                    call: Call<List<Movie>>,
                    response: Response<List<Movie>>
                ) {
                    if (response.body() != null) {
                        //把数据传递给PagedList
                        callback.onResult(response.body()!!)
                        Log.d("hqk", "loadInitial:" + response.body())
                    }
                }

                override fun onFailure(call: Call<List<Movie>>, t: Throwable) {}
            })
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Movie>) {
        RetrofitClient.getInstance()
            .getApi()
            .getMovies(params.key, PER_PAGE)
            .enqueue(object : Callback<List<Movie>> {
                override fun onResponse(
                    call: Call<List<Movie>>,
                    response: Response<List<Movie>>
                ) {
                    if (response.body() != null) {
                        //把数据传递给PagedList
                        callback.onResult(response.body()!!)
                        Log.d("hqk", "loadInitial:" + response.body())
                    }
                }
                override fun onFailure(call: Call<List<Movie>>, t: Throwable) {}
            })
    }
    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Movie>) {

    }
}

代码解析:

  • loadInitial与loadAfter 这两个方法,到这就不用再次讲解了吧
  • 不过注意的是,这里额外多了一个getKey方法!
  • 返回的是就是当条对应的id

因为UI使用代码与运行效果一样,因此这里就不再次贴对应代码以及对应运行效果了!

直接看后台打印日志

ItemKeyedDataSourceServlet doGet
since:0,pagesize:8
ItemKeyedDataSourceServlet doGet
since:34960094,pagesize:8
ItemKeyedDataSourceServlet doGet
since:27662747,pagesize:8
ItemKeyedDataSourceServlet doGet
since:34962956,pagesize:8
ItemKeyedDataSourceServlet doGet
since:30257787,pagesize:8
ItemKeyedDataSourceServlet doGet
since:25862300,pagesize:8
ItemKeyedDataSourceServlet doGet
since:26996524,pagesize:8
ItemKeyedDataSourceServlet doGet
since:30403683,pagesize:8

OK,越来越简单了!到这!已经全部讲完这三种方式的使用方法以及对应的特点!

以为这就完了?那当然肯定没完囖!

到现在为止!只实现了有网络的情况。而在没有网络情况,已经加载后的数据,重进app就直接over了!

那如果想要实现无网络加载缓存该怎么做呢???

这就要请出最后一位嘉宾到场!

5、BoundaryCallback

特点上面说了!那么它是怎么实现的呢?
在这里插入图片描述
如图所示

  • App通知BoundaryCallback,然后BoundaryCallback再去请求服务器,请求下来的数据经过“中间商-数据库”过一下手最后发送给UI刷新数据!
  • 因为有网的时候对应数据经过数据库时,就已经有缓存了,因此在没有网络的情况下就能直接加载缓存

这里服务端的代码使用4.2格式

这里说到使用数据库,那么!

5.1 对应数据表实体类

@Entity(tableName = "movie")
class Movie {


    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "no", typeAffinity = ColumnInfo.INTEGER)
    var NO = 0

    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    var id = 0

    @ColumnInfo(name = "title", typeAffinity = ColumnInfo.TEXT)
    var title: String? = null

    @ColumnInfo(name = "rate", typeAffinity = ColumnInfo.TEXT)
    var rate: String? = null

    @ColumnInfo(name = "cover", typeAffinity = ColumnInfo.TEXT)
    var cover: String? = null

}

因为用到了数据库,那么就不再只是之前普通的网络实体类了,同时还是访问数据库的实体类!

因为当前使用的是4.2格式的访问方式,因此

5.2 客服端解析json实体类

class Movies {

    @SerializedName("has_more")
    var hasMore = false

    @SerializedName("subjects")
    var movieList: List<Movie>? = null

}

对应API

interface Api {


//    @GET("pds.do")
//    fun getMovies(
//        @Query("start") since: Int,
//        @Query("count") perPage: Int
//    ): Call<Movies>

//    @GET("pkds.do")
//    fun getMovies(
//        @Query("page") page: Int,
//        @Query("pagesize") pagesize: Int
//    ): Call<Movies>

//    @GET("ikds.do")
//    fun getMovies(
//        @Query("since") since: Int,
//        @Query("pagesize") pagesize: Int
//    ): Call<List<Movie>>

    @GET("pkds.do")
    fun getMovies(
        @Query("page") page: Int,
        @Query("pagesize") pagesize: Int
    ): Call<Movies>
}

既然用到了数据库!那么

5.3 创建对应数据库

@Database(entities = [Movie::class], version = 1, exportSchema = true)
abstract class MyDatabase : RoomDatabase() {

    companion object {
        private const val DATABASE_NAME = "my_db.db"

        private var mInstance: MyDatabase? = null

        @Synchronized
        @JvmStatic
        fun getInstance(context: Context): MyDatabase {
            if (mInstance == null) {
                mInstance = Room.databaseBuilder(
                    context.applicationContext,
                    MyDatabase::class.java,
                    DATABASE_NAME
                ).build()
            }
            return mInstance as MyDatabase
        }
    }


    abstract fun getMovieDao(): MovieDao
}

这个在本专栏讲解过,这里不再详解了!

5.4 对应Dao

@Dao
interface MovieDao {

    @Insert
    fun insertMovies(movies: List<Movie>)

    @Query("DELETE FROM movie")
    fun clear()

    @Query("SELECT * FROM movie")
    fun getMovieList(): DataSource.Factory<Int, Movie>
}

注意

  • 这里查询所有的时候,返回格式为DataSource.Factory
  • 因此可以得出:在Room数据库里,已经自动帮我们实现了三大核心类的DataSource
  • 所以在之前的基础上,可以删除掉已经实现后的DataSource

现在就差最后的重点到场了

5.5 BoundaryCallback


class MovieBoundaryCallback : BoundaryCallback<Movie> {


    companion object {
        const val PER_PAGE = 8
        var FIRST_PAGE = 1
    }

    private var application: Application? = null

    constructor(application: Application) {
        this.application = application
    }

    override fun onZeroItemsLoaded() {
        super.onZeroItemsLoaded()
        //加载第一页数据!
        getTopData()
    }

    private fun getTopData() {
        RetrofitClient.getInstance()
            .getApi()
            .getMovies(FIRST_PAGE, PER_PAGE)
            .enqueue(object : Callback<Movies> {
                override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
                    if (response.body() != null) {
                        //把数据传递给PagedList
                        insertMovies(response.body()!!.movieList!!)
                        Log.d("hqk", "loadInitial:" + response.body())
                    }
                }

                override fun onFailure(call: Call<Movies>, t: Throwable) {

                }

            })

    }

    //把网络数据,保存到数据库
    private fun insertMovies(movies: List<Movie>) {
        thread {
            MyDatabase.getInstance(application!!)
                .getMovieDao()
                .insertMovies(movies)

        }
    }

    override fun onItemAtEndLoaded(movie: Movie) {
        super.onItemAtEndLoaded(movie)
        //加载第二页数据
        getTopAfterData()
    }

    private fun getTopAfterData() {
        FIRST_PAGE += 1
        RetrofitClient.getInstance()
            .getApi()
            .getMovies(FIRST_PAGE, PER_PAGE)
            .enqueue(object : Callback<Movies> {
                override fun onResponse(call: Call<Movies>, response: Response<Movies>) {
                    if (response.body() != null) {
                        insertMovies(response.body()!!.movieList!!)
                        Log.d("hqk", "loadInitial:" + response.body())
                    }
                }

                override fun onFailure(call: Call<Movies>, t: Throwable) {

                }

            })
    }
}

代码解析:

  • 这里我们看到:继承了BoundaryCallback
  • 重写了onZeroItemsLoaded与onItemAtEndLoaded方法;
  • 这两个方法分别表示加载第一页以及加载次页调用的方法
  • 每次加载完成时,两个方法都会调用insertMovies方法,将其数据添加至数据库里

这里看到,所有的网络数据全进入了数据库,既然有进,那就肯定有出!

因此,来看看

5.6 对应ViewModel

class MovieViewModel : AndroidViewModel {


    companion object {
        const val PER_PAGE = 8
    }

    var moviePagedList: LiveData<PagedList<Movie>>? = null

    constructor(application: Application) : super(application) {
        val movieDao: MovieDao = MyDatabase.getInstance(application).getMovieDao()
        moviePagedList =
            LivePagedListBuilder(movieDao.getMovieList(), PER_PAGE).setBoundaryCallback(
                MovieBoundaryCallback(application)
            ).build()
    }

    /**
     * 刷新
     */
    fun refresh() {
        viewModelScope.launch(Dispatchers.IO) {
            MyDatabase.getInstance(getApplication())
                .getMovieDao()
                .clear()
        }
    }

}

代码解析

这里我们可以看到:

  • 因为这里用到上下文,因此这里使用的是AndroidViewModel
  • 多了个refresh方法,用来清空数据库,达到刷新数据的作用
  • moviePagedList获取方式,依旧使用的是LivePagedListBuilder
  • 不同的是,构造参数不再是自己实现的DataSource.Factory,而是使用Room数据库里的DataSource.Factory
  • 并且还额外设置了setBoundaryCallback对象

最后!

5.7 来看看对应UI使用

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
        recyclerView.layoutManager = LinearLayoutManager(this)

        val adapter = MoviePagedListAdapter(this)
        recyclerView.adapter = adapter

        val movieViewModel = MovieViewModel(application)

        movieViewModel.moviePagedList!!.observe(this, Observer<PagedList<Movie>> {
            adapter.submitList(it)
        })

        findViewById<SwipeRefreshLayout>(R.id.swipeRefresh).also {
            it.setOnRefreshListener {
                movieViewModel.refresh()
                it.isRefreshing = false
            }
        }
    }
}

这里没啥可说的,

5.8 来看看使用效果

5.8.1 有网情况

在这里插入图片描述
刷新没问题!

5.8.2 无网缓存情况

在这里插入图片描述

这里看到在没有网络的情况,重进App依然能够加载已经加载过的缓存数据!

在这里插入图片描述
如图所示

在对应的数据库文件里,也能看到对应的数据大小在变动!

结束语

好了,到这里就真的结束了,相信看到这里的小伙伴对Jetpack—Paging有了深刻的理解!在下一篇中,将会结合Flow+Paging3进行讲解!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值