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.