preface

In the previous chapters, other components of Jetpack were mainly explained. In this chapter, Paging component will be explained.

1. Learn about Jetpack — Paging

1.1 Significance of the Paging component

Paging loading is a very common requirement in the process of application development. Paging is a component designed by Google for the convenience of Android developers to complete Paging loading. It provides a unified solution for several common Paging mechanisms and enables us to focus more experience on business code.

1.2 Architecture types supported by Paging

As is shown in

The architecture types supported by Paging:

  • Network data

    • Paging load of network data is one of the most common paging requirements, and it is also the focus of our study. Different companies often have different apis for paging, but in general there are three.
    • The Paging component provides three different scenarios to deal with different Paging mechanisms.
    • Respectively is:PositionDataSource, PageKeyedDataSource, ItemKeyedDataSource
  • The database

    • Once you’ve mastered network data paging, database data paging will be much easier, just replacing the data source
  • Network + Database

    • For user experience purposes, we typically cache network data so that the next time a user opens an application, the application displays the cached data first.
    • We often use databases to cache network data, which means we need to handle both network and database data sources.
    • Multiple data sources complicate business logic, so! We typically use a single data source as a solution.
    • While the data obtained from the network is cached directly into the database, the list only gets the data from the database, which is the only data sourceHere we will learnBoundaryCallback

1.4 Working principle of Paging

As is shown in

The three core classes of Paging

  • PagedListAdpater

    • RecycleView needs to be used with an adapter. If you want to use the Paging component, the adapter needs to inherit from PagedListAdpater
  • PagedList

    • PagedList is responsible for telling the DataSource when and how to get the data. For example, when to load the first/next page, how many pages to load the first page, how many data pieces to start preloading, etc., the data obtained from the DataSource will be stored in the PagedList
  • DataSource

    • Data can be loaded from the network or local database. Paging provides three DataSource types according to the Paging mechanism
    • The loading of data needs to take place in the worker thread

The concept finally said over, wide to begin actual combat!

2. Preparation

2.1 Basic Configuration

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'}... slightlydef 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. The paging: the paging - runtime - KTX: 2.1.2'
    implementation 'com. Squareup. Picasso was: Picasso was: 2.71828'. slightlyCopy the code

Note: Here we use paging: Paging version: 2.1.2. In the next Flow+Paging article, version 3+ will be used.

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(a): 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(a): OkHttpClient {
        return OkHttpClient.Builder().build()
    }
    fun getApi(a): Api {
        returnretrofit!! .create(Api::class.java)
    }
}
Copy the code

OK, we are ready. Instead of the three core classes mentioned above, we will explain them in the order illustrated above:

3. The three core classes of Paging

3.1 PagedListAdpater

// Analysis point 1
class MoviePagedListAdapter : PagedListAdapter<Movie, MovieViewHolder> {


    //None of the following functions can be called with the arguments supplied:

protected/*protected and package*/ constructor PagedListAdapter< T : Any! , VH : RecyclerView.ViewHolder! > (config: AsyncDifferConfig< Movie! >) defined in androidx.paging.PagedListAdapter

protected/*protected and package*/ constructor PagedListAdapter< T : Any! , VH : RecyclerView.ViewHolder! > (diffCallback: DiffUtil.ItemCallback< Movie! >) defined in androidx.paging.PagedListAdapter
private var context: Context? = null // Analysis point 2 constructor(context: Context) : super(DIFF_CALLBACK) { this.context = context } companion object { // Analysis point 3 // Difference, update only the elements that need to be updated, instead of refreshing the entire data source private val DIFF_CALLBACK: DiffUtil.ItemCallback<Movie> = object : DiffUtil.ItemCallback<Movie>() { // call to check whether two objects represent the same project. // True if two items represent the same object, false if they are different. override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean { return oldItem === newItem } // call to check whether two items have the same data. // This information is used to check whether the contents of the project have changed. // True if the contents of the items are the same, false if they are different. 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) { // A single entity class can be obtained directly from getItem val movie = getItem(position) if(movie ! =null) { / / use 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) } } }Copy the code

Code parsing:

  • Analysis Point 1: Here we can see that the Adapter corresponding to RecyclerView binding is not directly inheritedRecyclerView.Adapter, butPagedListAdapter<xxx, xxxHolder>
  • Analysis point 2: Of coursePagedListAdapter, you need to call the parent constructor and pass the corresponding parametersDIFF_CALLBACKAnd the corresponding parameter is defined at analysis point 3
  • Analysis point 3: We can see that theDIFF_CALLBACKProperty, inheritedDiffUtil.ItemCallback<Movie>Abstract class, and implement the corresponding abstract method. That method is differential data comparison, which means that when something changes, the UI only refreshes part of the change, not all of it!

This is the Adpater code, written here is more complete, all the following cases will use this one. OK, now for the second core class!

I’m using the Movie entity class,

class Movie {

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

3.2 PagedList

class MovieViewModel : ViewModel {

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

    constructor() : super() {
        val config = PagedList.Config.Builder() // Set the control placeholder
        	// Use this configuration to pass false to disable empty placeholders in PagedLists. 

If not set, the default is true.

.setEnablePlaceholders(false) .setPageSize(MovieDataSource.PER_PAGE) // Set the next page to start loading when there are still many rows to the bottom .setPrefetchDistance(2) // Set the number of loads for the first time .setInitialLoadSizeHint(MovieDataSource.PER_PAGE * 2) .setMaxSize(65536 * MovieDataSource.PER_PAGE) .build() moviePagedList = LivePagedListBuilder(MovieDataSourceFactory(), config).build() } } Copy the code

From this code we can see:

  • LiveData<PagedList<Movie>>Here we useLiveDataFrom this, we can see that the main responsibility of UI refresh lies inmoviePagedListVariable, and that variable is usedPagedList
  • moviePagedList =LivePagedListBuilder(MovieDataSourceFactory(), config).build()Here byLivePagedListBuilder.build()Assign the value to the corresponding LiveData data, the second parameterconfigThis is what is configured above
  • The first parameter is used hereMovieDataSourceFactory, so enterMovieDataSourceFactoryCheck it out!

MovieDataSourceFactory

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

Here we can see:

  • MovieDataSourceFactory inheritedDataSource.Factory<Int, Movie>()
  • And then in the rewrite create()And returned to theMovieDataSourceThe instance

So the third core class, DataSource, look!

3.3 the DataSource


class MovieDataSource : PositionalDataSource<Movie>() {

    companion object {
        const val PER_PAGE = 8
    }
    
    /** * Load the initial list data */
    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Movie>){}/** * call to load a series of data from the DataSource (load the next page of data) */
    override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<Movie>){}}Copy the code

We see

  • Here,MovieDataSource inheritedPositionalDataSource
  • Rewrite theloadInitialMethod, which means: called when the initial list data is loaded
  • Rewrite theloadRangeMethod, which means: called when data on the next page is loaded

The PositionalDataSource can only be inherited from the PositionalDataSource. If there is another way, what is the correspondence?

DataSource Three types of inheritance

Different inheritance methods, can achieve different Json paging effect!

Let’s start with the inherited PositionalDataSource

4.1 inheritance PositionalDataSource

So what are the features of an inherited PositionalDataSource?

As is shown in

This applies when data can be loaded from any location and the number of target data sources is fixed. For example, if start=2&count=5 is used in the request, it indicates that the request for the second data is extended five times ~

So!

4.1.1 Let’s take a look at the server-side code

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();
        // Retrieve data from a collection of MOVIES
        List<Movie> searchList = new ArrayList<>();
        for (int i = start; i < start + count; i++) {
            try{
                searchList.add(ServerStartupListener.MOVIES.get(i));
            }catch (IndexOutOfBoundsException e){
                // Index out of bounds, out of loop
                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(); }}Copy the code

Very primitive, but we can see the corresponding data structure:

{
    "count":8."start":0."total":100."subjects":[
        {
            "id":35076714."title":"Zack Snyder's Justice League."."cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp"."rate":"8.9"}}]Copy the code

so

4.1.2 Client parsing JSON entity class

class Movies {

    // The current number of returns
    var count = 0

    // Start position
    var start = 0

    // How many pieces are there
    var total = 0

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

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

Now that we have our entity class, let’s see what the final MovieDataSource looks like

4.1.3 MovieDataSource


class MovieDataSource : PositionalDataSource<Movie>() {

    companion object {
        const val PER_PAGE = 8
    }


    /** * Load the initial list data */
    override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Movie>) {
        val startPosition = 0

        RetrofitClient.getInstance()
            .getApi()
            //startPosition first; PER_PAGE Number of pages to be loaded on a page
            .getMovies(startPosition, PER_PAGE)
            .enqueue(object : Callback<Movies> {

                override fun onResponse(call: Call<Movies? >, response:Response<Movies? >) {
                    if(response.body() ! =null) {
                        // Pass the data to PagedListcallback.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 is the last one on the previous page, which is the first one on the next page; PER_PAGE Number of pages to be loaded on a page
            .getMovies(params.startPosition, PER_PAGE)
            .enqueue(object: Callback<Movies? > {override fun onResponse(call: Call<Movies? >, response:Response<Movies? >) {
                    if(response.body() ! =null) {
                        // Pass the data to PagedListcallback.onResult(response.body()!! .movieList!!) Log.d("hqk"."loadRange:"+ response.body()!! .movieList) } }override fun onFailure(call: Call<Movies? >, t:Throwable){}}}})Copy the code

Code parsing:

  • The loadInitial and loadRange methods mentioned above are the first loading method, the pagination loading method, and the callback. OnResult method returns the current requested network data

  • The callback corresponding to callback.onResult is LoadInitialCallback and LoadRangeCallback of the corresponding method

  • Instead of calling the getApi().getMovies method, look at this method:

    interface Api {
        @GET("pds.do")
        fun getMovies(
            @Query("start") since: Int.@Query("count") perPage: Int
        ): Call<Movies>
    }
    Copy the code
  • Here you can see the use of start and count, which represent the starting subscript of the corresponding page number and the number of pages per page respectively

  • In loadRange, params.startPosition represents the last subscript of the previous page and the start of the current page

Everything is ready except the UI!

4.1.4 UI Usage

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

        valmovieViewModel = MovieViewModel() movieViewModel.moviePagedList!! .observe(this, Observer<PagedList<Movie>> {
            adapter.submitList(it)
        })
    }
}
Copy the code

Here we see

  • Using theadapter.submitList(it)Code, and this method isPagedListAdapterWhat is provided inside
  • What it does: it corresponds toPagedListAdapterThe difference data inside are matched and displayedAdapterThe data of

Let’s see how it works

Corresponding background printing

PositionalDataSourceServlet doGet
start:0,count:8
PositionalDataSourceServlet doGet
start:8,count:8
Copy the code

In summary, when instantiating MovieViewModel, the corresponding DataSource will be accessed through the three core classes of Paging. First, the corresponding loadInitial method will be enabled to load the initial page. When the next page is loaded, the loadRange method will be called to load the data of the next page. Then modify the corresponding LiveData data, so use observe to notify UI to refresh the data!

Next comes the second method of inheritance!

4.2 inheritance PageKeyedDataSource

So what does inherit PageKeyedDataSource do?

As is shown in

This approach applies when the data source makes requests as pages. For example, if the parameters are page=2&pageSize=5, the data source has five data items on one page and currently returns five data items on the second page.

So!

4.2.1 Let’s take a look at the server-side code

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();
        // Retrieve data from a collection of 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){
                // Index out of bounds, out of loop
                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(); }}Copy the code

According to this code, the corresponding Json format is:

{
    "has_more":true."subjects":[
        {
            "id":35076714."title":"Zack Snyder's Justice League."."cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp"."rate":"8.9"}}]Copy the code

So!

4.2.2 Client parsing JSON entity class

class Movies {

    @SerializedName("has_more")
    var hasMore = false

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


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

Now that we have our entity class, let’s see what the final MovieDataSource looks like

Holdings 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) {
                        // Pass the data to PagedListcallback.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) {
                        // Pass the data to PagedList
                        val nextKey = if(response.body()!! .hasMore) params.key +1 else nullcallback.onResult(response.body()!! .movieList!! , nextKey) Log.d("hqk"."loadInitial loadAfter :"+ response.body()!! .movieList) } }override fun onFailure(call: Call<Movies? >, t:Throwable){}}}})Copy the code

Code parsing:

  • The loadInitial and loadAfter methods are first loaded, page-loaded and called respectively, and the network data currently requested is returned via callback.onResult

  • The callback corresponding to callback.onResult is LoadInitialCallback and LoadCallback of the corresponding method

  • Instead of calling the getApi().getMovies method, look at this method:

    interface Api {
    
    
    // @GET("pds.do")
    // fun getMovies(
    // @Query("start") since: Int,
    // @Query("count") perPage: Int
    // ): Call
            
    
        @GET("pkds.do")
        fun getMovies(
            @Query("page") page: Int.@Query("pagesize") pagesize: Int
        ): Call<Movies>
    }
    Copy the code
  • Here you can see page and pagesize used, representing the number of pages and the number of pages per page, respectively

  • In the loadInitial method, when callback.onResult is called after the home page is loaded, it passes in the moviedatasource.first_page + 1 to load the data directly from page 2 next time

  • In the loadAfter method, response.body()!! .hasmore indicates whether there is a next page, which determines whether the next page is +1

Everything is ready except the UI!

Because the UI uses the same code and runs the same, so I will not paste the corresponding code and the corresponding run the effect again!

View the background print logs directly

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
Copy the code

OK, it’s getting easier and easier! Strike while the iron is hot, let’s see one last use!

4.3 inheritance ItemKeyedDataSource

Or old rules, look at the corresponding characteristics first!

As is shown in

The current approach applies to: This type of paging is common in commenting when the next page of the target data depends on a key from the last object on the previous page. For example, if the key of the last object on the previous page is 9001, then when requesting the next page, If the shoelace parameter since= 9001&pagesize =5 is required, the server will return five data items after key=9001

So!

4.3.1 Let’s take a look at the server-side code

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);

        // Retrieve data from a collection of MOVIES
        List<Movie> searchList = new ArrayList<>();
        // On the first request, since equals 0, reassign since to the id of the first element
        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{
                // Find the element whose ID is equal to since with the request parameter since
                // Find the pagesize element later
                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){
                // Index out of bounds, out of loop
                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(); }}Copy the code

According to this code, the corresponding Json format is:

[{"id":35076714."title":"Zack Snyder's Justice League."."cover":"https://img9.doubanio.com/view/photo/s_ratio_poster/public/p2634360594.webp"."rate":"8.9"}]Copy the code

So use the Movie entity class, and just wrap a list around it!

Then you can look directly at the corresponding 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) {
                        // Pass the data to 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) {
                        // Pass the data to 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>){}}Copy the code

Code parsing:

  • LoadInitial and loadAfterI don’t have to go over these two methods again
  • But notice, there’s an extra one heregetKeyMethods!
  • Returns the id of the corresponding bar

Because the UI uses the same code and runs the same, so I will not paste the corresponding code and the corresponding run the effect again!

View the background print logs directly

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
Copy the code

OK, it’s getting easier! To this! Had told the characteristic that the use method of these 3 kinds of means and correspondence completely!

Think this is the end? That is certainly not over!

By now! Only the network case is implemented. In the case of no network, the data that has been loaded will be directly over when re-entering the APP!

What do you do if you want to implement netless load caching?

This is the last guest to show up!

5, BoundaryCallback

Features said above! So how does it work?

As is shown in

  • App notificationsBoundaryCallbackAnd thenBoundaryCallbackTo request the server, request down the data through the “middleman – database” over a hand finally sent to the UI refresh data!
  • Because there is already a cache when the data passes through the database with the network, the cache can be loaded directly without the network

The server code here is in 4.2 format

Speaking of using a database, then!

5.1 Corresponding data table entity class

@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

}
Copy the code

Because of the use of the database, so no longer just before the ordinary network entity class, at the same time or access to the database entity class!

Because 4.2 format access is currently used, so

5.2 Client Parsing JSON entity Class

class Movies {

    @SerializedName("has_more")
    var hasMore = false

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

}
Copy the code

Corresponding to the API

interface Api {


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

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

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

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

Now that you’re using a database! then

5.3 Creating a Database

@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(a): MovieDao
}
Copy the code

This has been explained in this column, but I won’t go into detail here!

5.4 the corresponding Dao

@Dao
interface MovieDao {

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

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

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

Pay attention to

  • When all is queried here, the return format is DataSource.Factory
  • Therefore, it can be concluded that the Room database has automatically helped us to achieve the three core classesDataSource
  • So on the basis of the previous, you can delete the implementedDataSource

Now we just need to get to the last point

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(a) {
        super.onZeroItemsLoaded()
        // Load the first page of data!
        getTopData()
    }

    private fun getTopData(a) {
        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) {
                        // Pass the data to PagedListinsertMovies(response.body()!! .movieList!!) Log.d("hqk"."loadInitial:" + response.body())
                    }
                }

                override fun onFailure(call: Call<Movies>, t: Throwable){}})}// Save network data to database
    private fun insertMovies(movies: List<Movie>) {
        thread {
            MyDatabase.getInstance(application!!)
                .getMovieDao()
                .insertMovies(movies)

        }
    }

    override fun onItemAtEndLoaded(movie: Movie) {
        super.onItemAtEndLoaded(movie)
        // Load the second page
        getTopAfterData()
    }

    private fun getTopAfterData(a) {
        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){}})}}Copy the code

Code parsing:

  • Here we see: inheritanceBoundaryCallback;
  • Rewrite theOnZeroItemsLoaded and onItemAtEndLoadedMethods;
  • These two methods represent the methods invoked when the first page is loaded and the next page is loaded, respectively
  • Both methods are called each time the load completesinsertMoviesMethod to add their data to the database

See here, all network data entered database completely, since have into, that certainly have go out!

So let’s see

5.6 the corresponding 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()
    }

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

}
Copy the code

Code parsing

Here we can see:

  • Because we’re using context here, we’re usingAndroidViewModel
  • More than arefreshMethod, used to empty the database, to achieve the function of refreshing data
  • whilemoviePagedList And the way to get it, I still useLivePagedListBuilder;
  • The difference is that the construct parameters are no longer self-implementedDataSource.Factory, but using the Room databaseDataSource.Factory
  • And there are additional SettingssetBoundaryCallbackobject

At last!

5.7 Take a look at the corresponding UI usage

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

        valmovieViewModel = 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}}}}Copy the code

There’s nothing to say here,

5.8 Take a look at the effect

5.8.1 Network Availability

Refresh is ok!

5.8.2 No Network Cache

Here you can see that in the case of no network, the reload App can still load cached data already loaded!

As is shown in

In the corresponding database file, you can also see the corresponding data size changes!

Demo address (including server) :Let me download

conclusion

Well, this is really the end of the jetpack-Paging2, I believe that you have a deep understanding of jetpack-Paging2! In the next article, we will combine Flow+Paging3 to explain (because Paging3 has changed a little bit, so we will use it separately. Finally, there will be a comprehensive exercise that will combine all of the previous ones, as well as a new addition to Jetpack, AppStartUp