Implementação de Paginação e Carregamento Contínuo no RecyclerView

Diferente do antigo ListView, o RecyclerView não possui um método nativo simplificado para adicionar um FooterView destinado ao carregamento de mais dados. Muitas implementações recorrem ao addScrollListener, o que frequentemente resulta em um código complexo, repleto de flags de controle e difícil manutenção. Uma alternativa mais elegente é gerenciar o estado de carregamento diretamente no Adapter, tratando o rodapé como um tipo de item específico.

Estrutura do Adapter Base

Abaixo, apresentamos uma classe abstrata que encapsula a lógica de paginação. Ela gerencia automaticamente a exibição de um endicador de progresso ou uma mensagem de fim de lista com base na quantidade de itens retornados.


public abstract class BasePaginationAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    protected List<T> dataList = new ArrayList<>();
    protected Context context;
    
    private boolean isFetching = false;
    private boolean canLoadMore = false;
    private int pageSize = 10;
    private int currentPage = 1;

    private static final int TYPE_CONTENT = 0;
    private static final int TYPE_FOOTER = 1;

    private OnPaginationListener paginationListener;

    public BasePaginationAdapter(Context context, int pageSize) {
        this.context = context;
        this.pageSize = pageSize;
    }

    public void setPaginationListener(OnPaginationListener listener) {
        this.paginationListener = listener;
    }

    public void resetData(List<T> newData) {
        dataList.clear();
        currentPage = 1;
        isFetching = false;
        if (newData != null) {
            canLoadMore = newData.size() >= pageSize;
            dataList.addAll(newData);
        }
        notifyDataSetChanged();
    }

    public void addMoreData(List<T> additionalData) {
        if (additionalData != null) {
            int startPos = dataList.size();
            canLoadMore = additionalData.size() >= pageSize;
            dataList.addAll(additionalData);
            
            if (paginationListener != null) {
                notifyItemRangeChanged(startPos, additionalData.size() + 1);
            } else {
                notifyItemRangeInserted(startPos, additionalData.size());
            }
        }
        isFetching = false;
    }

    @Override
    public int getItemViewType(int position) {
        if (paginationListener != null && position == getItemCount() - 1) {
            return TYPE_FOOTER;
        }
        return TYPE_CONTENT;
    }

    @Override
    public int getItemCount() {
        if (dataList == null) return 0;
        return paginationListener == null ? dataList.size() : dataList.size() + 1;
    }

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

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (getItemViewType(position) == TYPE_FOOTER) {
            FooterHolder footer = (FooterHolder) holder;
            footer.updateState(canLoadMore);
            
            if (!isFetching && canLoadMore && paginationListener != null) {
                isFetching = true;
                paginationListener.onLoadNextPage(++currentPage);
            }
        } else {
            onBindItemData(holder, dataList.get(position));
        }
    }

    protected abstract RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType);
    protected abstract void onBindItemData(RecyclerView.ViewHolder holder, T item);

    public interface OnPaginationListener {
        void onLoadNextPage(int page);
    }

    class FooterHolder extends RecyclerView.ViewHolder {
        View containerLoading;
        View labelNoMore;

        public FooterHolder(View itemView) {
            super(itemView);
            containerLoading = itemView.findViewById(R.id.container_progress);
            labelNoMore = itemView.findViewById(R.id.label_end_reached);
        }

        public void updateState(boolean hasMore) {
            containerLoading.setVisibility(hasMore ? View.VISIBLE : View.GONE);
            labelNoMore.setVisibility(hasMore ? View.GONE : View.VISIBLE);
        }
    }
}

Layout do Rodapé (layout_footer_loading.xml)

Este layout define visualmente o que o usuário verá ao chegar ao fim da lista. Ele alterna entre um ProgressBar e um TextView.


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="12dp">

    <LinearLayout
        android:id="@+id/container_progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:orientation="horizontal">

        <ProgressBar
            android:layout_width="24dp"
            android:layout_height="24dp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:text="Carregando..." />
    </LinearLayout>

    <TextView
        android:id="@+id/label_end_reached"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="Fim da lista"
        android:visibility="gone" />

</FrameLayout>

Exemplo de Implementação

Para utilizar o adapter, basta estendê-lo e implemetnar a lógica específica dos seus itens de dados. O gerenciamento da paginação já está embutido na classe base.


public class ProductAdapter extends BasePaginationAdapter<ProductModel> {

    public ProductAdapter(Context context) {
        super(context, 15); // Define o tamanho da página como 15
    }

    @Override
    protected RecyclerView.ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false);
        return new ProductViewHolder(view);
    }

    @Override
    protected void onBindItemData(RecyclerView.ViewHolder holder, ProductModel product) {
        ProductViewHolder vh = (ProductViewHolder) holder;
        vh.name.setText(product.getName());
        vh.price.setText(product.getFormattedPrice());
    }

    static class ProductViewHolder extends RecyclerView.ViewHolder {
        TextView name, price;

        public ProductViewHolder(View itemView) {
            super(itemView);
            name = itemView.findViewById(R.id.txt_product_name);
            price = itemView.findViewById(R.id.txt_product_price);
        }
    }
}

Ao implementar o OnPaginationListener na sua Activity ou Fragment, o método onLoadNextPage será disparado automaticamente sempre que o usuário atingir o último item e houver mais dados para carregar. Isso mantém o código da View limpo e focado na lógica de negócio.

Tags: android RecyclerView java mobile-development ui-components

Publicado em 7-19 01:29