Implementing Paginated Lists with Retrofit and Android Paging Library
Dependency Configuration
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.picasso:picasso:2.71828"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.paging:paging-runtime:3.1.1"
Required permission in AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
API Service Definition
package com.example.film.network;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class ServiceGenerator {
private static final String API_BASE_URL = "http://192.168.202.55:8999/";
private static ServiceGenerator instance;
private final Retrofit retrofitClient;
private ServiceGenerator() {
OkHttpClient httpClient = new OkHttpClient.Builder().build();
retrofitClient = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(httpClient)
.build();
}
public static synchronized ServiceGenerator getGenerator() {
if (instance == null) {
instance = new ServiceGenerator();
}
return instance;
}
public FilmApi getFilmApi() {
return retrofitClient.create(FilmApi.class);
}
}
package com.example.film.network;
import com.example.film.entity.FilmContainer;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
public interface FilmApi {
@GET("/findList")
Call<FilmContainer> fetchFilms(
@Query("start") int startIndex,
@Query("count") int pageSize
);
}
Data Models
package com.example.film.entity;
import java.util.Objects;
public class FilmItem {
public int filmId;
public String name;
public String score;
public String posterUrl;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FilmItem filmItem = (FilmItem) o;
return filmId == filmItem.filmId &&
Objects.equals(name, filmItem.name) &&
Objects.equals(score, filmItem.score) &&
Objects.equals(posterUrl, filmItem.posterUrl);
}
@Override
public int hashCode() {
return Objects.hash(filmId, name, score, posterUrl);
}
}
package com.example.film.entity;
import com.google.gson.annotations.SerializedName;
import java.util.List;
import java.util.Objects;
public class FilmContainer {
private Integer resultCount;
private Integer offset;
private Integer totalResults;
@SerializedName("subjects")
private List<FilmItem> items;
public List<FilmItem> getItems() { return items; }
public void setItems(List<FilmItem> items) { this.items = items; }
public Integer getTotalResults() { return totalResults; }
public void setTotalResults(Integer totalResults) { this.totalResults = totalResults; }
public Integer getOffset() { return offset; }
public void setOffset(Integer offset) { this.offset = offset; }
}
Data Source Implementation
package com.example.film.paging;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.paging.PositionalDataSource;
import com.example.film.entity.FilmContainer;
import com.example.film.entity.FilmItem;
import com.example.film.network.ServiceGenerator;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class FilmDataSource extends PositionalDataSource<FilmItem> {
public static final int LIMIT = 8;
@Override
public void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<FilmItem> cb) {
ServiceGenerator.getGenerator().getFilmApi()
.fetchFilms(0, LIMIT)
.enqueue(new Callback<FilmContainer>() {
@Override
public void onResponse(Call<FilmContainer> call, Response<FilmContainer> response) {
if (response.isSuccessful() && response.body() != null) {
FilmContainer data = response.body();
cb.onResult(data.getItems(), data.getOffset(), data.getTotalResults());
Log.d("FilmDataSource", "Initial load: " + data.getItems().size() + " items");
}
}
@Override
public void onFailure(Call<FilmContainer> call, Throwable t) {
Log.e("FilmDataSource", "Initial load failed", t);
}
});
}
@Override
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<FilmItem> cb) {
ServiceGenerator.getGenerator().getFilmApi()
.fetchFilms(params.startPosition, LIMIT)
.enqueue(new Callback<FilmContainer>() {
@Override
public void onResponse(Call<FilmContainer> call, Response<FilmContainer> response) {
if (response.isSuccessful() && response.body() != null) {
cb.onResult(response.body().getItems());
Log.d("FilmDataSource", "Range load position: " + params.startPosition);
}
}
@Override
public void onFailure(Call<FilmContainer> call, Throwable t) {
Log.e("FilmDataSource", "Range load failed", t);
}
});
}
}
Data Source Factory
package com.example.film.paging;
import androidx.annotation.NonNull;
import androidx.paging.DataSource;
import com.example.film.entity.FilmItem;
public class FilmFactory extends DataSource.Factory<Integer, FilmItem> {
@NonNull
@Override
public DataSource<Integer, FilmItem> create() {
return new FilmDataSource();
}
}
Adapter Configuration
package com.example.film.paging;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.squareup.picasso.Picasso;
import com.example.film.R;
import com.example.film.entity.FilmItem;
public class FilmAdapter extends PagedListAdapter<FilmItem, FilmAdapter.ViewHolder> {
private final Context appContext;
private static final DiffUtil.ItemCallback<FilmItem> DIFF_UTIL = new DiffUtil.ItemCallback<FilmItem>() {
@Override
public boolean areItemsTheSame(@NonNull FilmItem oldItem, @NonNull FilmItem newItem) {
return oldItem.filmId == newItem.filmId;
}
@Override
public boolean areContentsTheSame(@NonNull FilmItem oldItem, @NonNull FilmItem newItem) {
return oldItem.equals(newItem);
}
};
public FilmAdapter(Context context) {
super(DIFF_UTIL);
this.appContext = context;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(appContext).inflate(R.layout.item_film, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
FilmItem current = getItem(position);
if (current == null) return;
Picasso.get()
.load(current.posterUrl)
.placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_error)
.into(holder.poster);
String displayTitle = current.name.length() > 8 ? current.name.substring(0, 8) + "..." : current.name;
holder.title.setText(displayTitle);
holder.rating.setText(current.score);
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView poster;
TextView title, rating;
ViewHolder(@NonNull View itemView) {
super(itemView);
poster = itemView.findViewById(R.id.img_poster);
title = itemView.findViewById(R.id.txt_title);
rating = itemView.findViewById(R.id.txt_rating);
}
}
}
ViewModel Setup
package com.example.film.paging;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import com.example.film.entity.FilmItem;
public class FilmViewModel extends ViewModel {
public final LiveData<PagedList<FilmItem>> liveData;
public FilmViewModel() {
PagedList.Config config = new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(FilmDataSource.LIMIT)
.setPrefetchDistance(2)
.setInitialLoadSizeHint(FilmDataSource.LIMIT * 2)
.setMaxSize(100000)
.build();
liveData = new LivePagedListBuilder<>(new FilmFactory(), config).build();
}
}
Activiyt Integrasion
package com.example.film;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.paging.PagedList;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.film.paging.FilmAdapter;
import com.example.film.paging.FilmViewModel;
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
RecyclerView rv = findViewById(R.id.recycler_view);
rv.setLayoutManager(new LinearLayoutManager(this));
FilmAdapter filmAdapter = new FilmAdapter(this);
rv.setAdapter(filmAdapter);
FilmViewModel viewModel = new ViewModelProvider(this).get(FilmViewModel.class);
viewModel.liveData.observe(this, films -> {
if (films != null) {
filmAdapter.submitList(films);
}
});
}
}