Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Paginated Lists with Retrofit and Android Paging Library

Tech May 13 1

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);
            }
        });
    }
}
Tags: Android

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.