Paging is essential when a large amount of data needs to be loaded. In general, paging allows you to load and display some data on demand, which reduces network bandwidth usage and takes the strain off the server.

Paging is a part of Android Jetpack component and is used to realize Paging. It is very convenient and elegant to gradually load data into RecyclerView. I will use Paging 2.x version below and introduce this library. The application of LiveData, ViewModel, RecyclerView and RetroFIT in MVVM architecture is introduced.

1. Install

def paging_version = 2.1.2 ""
implementation "androidx.paging:paging-runtime:$paging_version" 
testImplementation "androidx.paging:paging-common:$paging_version" 
Copy the code

2. Introduction of main classes

2.1 PagedList

As the user slides down, the data source will be accessed and more application data will be loaded. At this time, the data will be dynamically loaded into the PageList under observation (this paper uses LiveData to observe the PagedList), and the changes will be dynamically reflected in the application interface.

2.2 the DataSource

A DataSource is either a network database or a local database, in which the implementation of paging is defined. A Factory subclass is also defined to generate a DataSource to initialize the corresponding PagedList.

2.3 PagedListAdapter

PagedList uses PagedListAdapter to load data to RecyclerView for display, at the same time to realize scrolling to a specific area to load the next page of data, which also defines the update, delete, add animation effect, in which also defines DIFF_CALLBACK, Enable it to automatically handle paging and list differences.

This is a very graphic picture.

3 Paging + LiveData + ViewModel + RecyclerView + Retrofit

Dependencies:

implementation 'androidx. Appcompat: appcompat: 1.2.0'
implementation 'androidx. Constraintlayout: constraintlayout: 2.0.2'
implementation 'androidx. Lifecycle: lifecycle - extensions: 2.2.0'

def paging_version = 2.1.2 ""
implementation "androidx.paging:paging-runtime:$paging_version"

def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "Com. Squareup. Okhttp3: okhttp: 4.8.1"
implementation "Com. Squareup. Okhttp3: logging - interceptor: 4.8.1"
implementation 'com. Google. Code. Gson: gson: 2.8.6'
implementation 'com. Squareup. Retrofit2: converter - gson: 2.9.0' / / retrofit

implementation 'androidx. Lifecycle: lifecycle - extensions: 2.2.0'
implementation "Androidx. Lifecycle: lifecycle - viewmodel: 2.2.0." "

implementation 'androidx. Recyclerview: recyclerview: 1.1.0'

implementation("Com. Making. Bumptech. Glide: glide: 4.11.0") {
    exclude group: "com.android.support"
}
Copy the code

3.1 GitHub API

This test uses the following interfaces provided by GitHub.

Interface: https://api.github.com/search/repositories

Search parameters are as follows:

q: The search parameters
sort: Sort parameters
page: The page number
per_page: Number each page
Copy the code

Select some of the data to encapsulate as class GithubRes:

public class GithubRes {
    @SerializedName("incomplete_results")
    public boolean complete;
    @SerializedName("items")
    public List<Item> items;
    @SerializedName("total_count")
    public int count;

    public static class Item {
        @SerializedName("id")
        public long id;
        @SerializedName("name")
        public String name;
        @SerializedName("owner")
        public Owner owner;
        @SerializedName("description")
        public String description;
        @SerializedName("stargazers_count")
        public int starCount;
        @SerializedName("forks_count")
        public int forkCount;

        public static class Owner {
            @SerializedName("login")
            public String name;
            @SerializedName("avatar_url")
            public String avatar;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null|| getClass() ! = o.getClass())return false;
            Item item = (Item) o;
            return id == item.id &&
                    starCount == item.starCount &&
                    forkCount == item.forkCount &&
                    Objects.equals(name, item.name) &&
                    Objects.equals(owner, item.owner) &&
                    Objects.equals(description, item.description);
        }

        @Override
        public int hashCode(a) {
            returnObjects.hash(id, name, owner, description, starCount, forkCount); }}}Copy the code

3.2 Defining Retrofit requests

This part mainly involves the definition of request parameters and the construction of retrofit factory classes.

GithubService:

public interface GithubService {

    @GET("search/repositories")
    Call<GithubRes> query(@Query("q") String query, @Query("sort") String sort,
                           @Query("page") int page, @Query("per_page") int size);
}
Copy the code

RetrofitServiceFactory:

public class RetrofitServiceFactory {
    private static final RetrofitServiceFactory INSTANCE = new RetrofitServiceFactory();
    private static Retrofit retrofit;

    private RetrofitServiceFactory(a) {
        OkHttpClient okHttpClient = new OkHttpClient().newBuilder()//
                .retryOnConnectionFailure(true)
                .build();
        retrofit = new Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .client(okHttpClient)
                .baseUrl("https://api.github.com/")
                .build();
    }

    public static RetrofitServiceFactory getInstance(a) {
        return INSTANCE;
    }

    public GithubService getGithubService(a) {
        returnretrofit.create(GithubService.class); }}Copy the code

Note that the factory class is frequently used in actual projects to obtain the corresponding service instance, so the singleton pattern should be used.

3.3 Defining a Data Source

There are three types of data sources:

  1. PageKeyedDataSource: If the previous page and next page options are provided, this data source can be used, which populates pages of data requested from the network.
  2. ItemKeyedDataSource: If you need to use data from project N to get project N+1, use this data source;
  3. PositionalDataSource: To obtain data from any location in data store 1, select this data source.

This test uses the PageKeyedDataSource data source.

GithubDataSource:

public class GithubDataSource extends PageKeyedDataSource<Integer.GithubRes.Item> {
    private static final int FIRST_PAGE = 1;
    public static final int PAGE_SIZE = 10;
    private GithubService githubService;
    private String query;
    private String sort;

    public GithubDataSource(GithubService githubService, String query, String sort) {
        this.githubService = githubService;
        this.query = query;
        this.sort = sort;
    }

    @Override
    public void loadInitial(@NonNull LoadInitialParams<Integer> params, @NonNull LoadInitialCallback<Integer, GithubRes.Item> callback) {
        Call<GithubRes> call = githubService.query(query, sort, FIRST_PAGE, PAGE_SIZE);
        try {
            Response<GithubRes> resResponse = call.execute();
            GithubRes res = resResponse.body();

            if(res ! =null) {
                callback.onResult(res.items, null, FIRST_PAGE + 1); }}catch(IOException e) { e.printStackTrace(); }}@Override
    public void loadBefore(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, GithubRes.Item> callback) {
        Call<GithubRes> call = githubService.query(query, sort, params.key, PAGE_SIZE);
        try {
            Integer key = (params.key > 1)? params.key -1 : null;
            GithubRes res = call.execute().body();
            if(res ! =null) { callback.onResult(res.items, key); }}catch(IOException e) { e.printStackTrace(); }}@Override
    public void loadAfter(@NonNull LoadParams<Integer> params, @NonNull LoadCallback<Integer, GithubRes.Item> callback) {
        Call<GithubRes> call = githubService.query(query, sort, params.key, PAGE_SIZE);
        try {
            GithubRes res = call.execute().body();
            if(res ! =null) {
                if(! res.complete) callback.onResult(res.items, params.key +1);
                else callback.onResult(res.items, null); }}catch(IOException e) { e.printStackTrace(); }}}Copy the code

Note that the synchronous method should be used for data requests, otherwise the flicker problem will occur when refreshing, because the Paging library has wrapped thread processing for us and does not need to use the asynchronous method.

GithubDataSourceFactory:

public class GithubDataSourceFactory extends DataSource.Factory<Integer.GithubRes.Item> {
    // To get the newly created GithubDataSource instance
    private MutableLiveData<GithubDataSource> mGithubDataSource = new MutableLiveData<>(); 
    private String query;
    private String sort;

    public GithubDataSourceFactory(String query, String sort) {
        this.query = query;
        this.sort = sort;
    }

    @NonNull
    @Override
    public DataSource<Integer, GithubRes.Item> create() {
        GithubDataSource githubDataSource = new GithubDataSource(RetrofitServiceFactory.getInstance().getGithubService(),query,sort);
        mGithubDataSource.setValue(githubDataSource);
        return githubDataSource;
    }

    public MutableLiveData<GithubDataSource> getmGithubDataSource(a) {
        returnmGithubDataSource; }}Copy the code

Now that our data source has been defined, we need to reflect the data to the interface.

3.4 Defining the Layout

We need an overall layout and a recyclerview item layout.

MainActivity:


      
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <EditText
            android:id="@+id/search_repo"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:hint="Please enter search content"
            android:imeOptions="actionSearch"
            android:inputType="textNoSuggestions"
            android:selectAllOnFocus="true"
            android:text="Android"/>
        <Button
            android:id="@+id/search"
            android:text="Search"
            android:backgroundTint="@color/colorAccent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>
Copy the code

item:


      
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:baselineAligned="false"
    android:padding="10dp">
    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_weight="1">
        <TextView
            android:id="@+id/project_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="start"
            android:textSize="16sp"/>
        <TextView
            android:id="@+id/project_description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="start"
            android:textSize="14sp"/>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="15dp">
            <ImageView
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:src="@drawable/star"
                android:contentDescription="star" />
            <TextView
                android:id="@+id/star_count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="14sp"
                android:textColor="@color/colorPrimary"
                android:layout_marginEnd="20dp"/>
            <ImageView
                android:layout_width="20dp"
                android:layout_height="20dp"
                android:src="@drawable/fork"
                android:contentDescription="fork" />
            <TextView
                android:id="@+id/fork_count"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="14sp"
                android:textColor="@color/colorPrimary"/>
        </LinearLayout>
    </LinearLayout>

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/owner_avatar"
            android:layout_width="60dp"
            android:layout_height="60dp"/>
        <TextView
            android:id="@+id/owner_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textSize="16sp"/>
    </LinearLayout>
</LinearLayout>
Copy the code

3.5 Defining an Adapter

The adapter inherits the PagedListAdapter and defines diffUtil.ItemCallback to implement differential callbacks that automatically handle paging and list differences, and the adapter automatically detects changes to these items when a new PagedList object is loaded. The adapter then triggers a valid project animation within the RecyclerView object.

public class GithubPagedAdapter extends PagedListAdapter<GithubRes.Item.RecyclerView.ViewHolder> {
    private final Context mContext;

    protected GithubPagedAdapter(Context context) {
        super(DIFF_CALLBACK);
        mContext = context;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
        return new ContentViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        GithubRes.Item item = getItem(position);
        if (item == null)
            return;
        ContentViewHolder contentViewHolder = (ContentViewHolder) holder;
        contentViewHolder.forkCount.setText(String.valueOf(item.forkCount));
        contentViewHolder.starCount.setText(String.valueOf(item.starCount));
        contentViewHolder.projectDescription.setText(String.valueOf(item.description));
        contentViewHolder.ownerName.setText(String.valueOf(item.owner.name));
        contentViewHolder.projectName.setText(String.valueOf(item.name));
        Glide.with(mContext).load(item.owner.avatar)
                .override(contentViewHolder.ownerAvatar.getWidth(), contentViewHolder.ownerAvatar.getHeight())
                .into(contentViewHolder.ownerAvatar);
    }

    public static class ContentViewHolder extends RecyclerView.ViewHolder {
        public ImageView ownerAvatar;
        public TextView projectName;
        public TextView ownerName;
        public TextView projectDescription;
        public TextView starCount;
        public TextView forkCount;

        public ContentViewHolder(@NonNull View itemView) {
            super(itemView); ownerAvatar = itemView.findViewById(R.id.owner_avatar); projectName = itemView.findViewById(R.id.project_name); ownerName = itemView.findViewById(R.id.owner_name); projectDescription = itemView.findViewById(R.id.project_description); starCount = itemView.findViewById(R.id.star_count); forkCount = itemView.findViewById(R.id.fork_count); }}private final static DiffUtil.ItemCallback<GithubRes.Item> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<GithubRes.Item>() {
                @Override
                public boolean areItemsTheSame(@NonNull GithubRes.Item oldItem, @NonNull GithubRes.Item newItem) {
                    return oldItem.id == newItem.id;
                }

                @Override
                public boolean areContentsTheSame(@NonNull GithubRes.Item oldItem, @NonNull GithubRes.Item newItem) {
                    returnoldItem.equals(newItem); }}; }Copy the code

3.6 build PagedList

Next, you just need to build the PagedList in the ViewModel and observe it using LiveData.

public class MainViewModel extends ViewModel {
    private LiveData<PagedList<GithubRes.Item>> itemPagedList;

    public void setItemPagedList(String query, String sort) {
        GithubDataSourceFactory factory = new GithubDataSourceFactory(query, sort);
        PagedList.Config myPagingConfig = new PagedList.Config.Builder()
                .setPageSize(10)
                .setPrefetchDistance(30)
                .setEnablePlaceholders(true)
                .build();
        itemPagedList = new LivePagedListBuilder<>(factory, myPagingConfig)
                .build();
    }

    public LiveData<PagedList<GithubRes.Item>> getItemPagedList() {
        returnitemPagedList; }}Copy the code

3.7 Loading an Activity

public class MainActivity extends AppCompatActivity {
    private MainViewModel mainViewModel;
    private GithubPagedAdapter mGithubPagedAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
        EditText editText = findViewById(R.id.search_repo);
        Button button = findViewById(R.id.search);
        button.setOnClickListener(v -> {
            mainViewModel.setItemPagedList(editText.getText().toString(), "stars");
            setRecyclerView();
        });

    }

    private void setRecyclerView(a) {
        mGithubPagedAdapter = new GithubPagedAdapter(this);
        mainViewModel.getItemPagedList().observe(this, items -> {
            mGithubPagedAdapter.submitList(items);
        });
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); RecyclerView recyclerView = findViewById(R.id.recycler_view); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(mGithubPagedAdapter); }}Copy the code

Running results:

The results look good! However, if used in a real project, we also need to add a refresh function and consider handling request errors, which I will cover in the next Android Paging component error handling article.

4 source

Source code has been uploaded to Github, welcome star.

5 reference

  1. The Android developers web site

  2. Android Paging Library Step By Step Implementation Guide