Endless RecyclerView with ProgressBar for pagination

AndroidAndroid ListviewAndroid RecyclerviewAndroid 5.0-LollipopAndroid Progressbar

Android Problem Overview


I am using a RecyclerView and fetching objects from an API in batches of ten. For pagination, I use EndlessRecyclerOnScrollListener.

It's all working properly. Now all that's left is to add a progress spinner at the bottom of the list while the next batch of objects is fetched by the API. Here is a screenshot of the Google Play Store app, showing a ProgressBar in what is surely a RecyclerView:

enter image description here

The problem is, neither the RecyclerView nor the EndlessRecyclerOnScrollListener have built-in support for showing a ProgressBar at the bottom while the next batch of objects is being fetched.

I have already seen the following answers:

1. Put an indeterminate ProgressBar as footer in a RecyclerView grid.

2. Adding items to Endless Scroll RecyclerView with ProgressBar at bottom.

I am not satisfied with those answers (both by the same person). This involves shoehorning a null object into the data-set midway while the user is scrolling and then taking it out after the next batch is delivered. It looks like a hack that sidesteps the main problem which may or may not work properly. And it causes a bit of jarring and distortion in the list

Using SwipeRefreshLayout is not a solution here. SwipeRefreshLayout involves pulling from the top to fetch the newest items, and it does not show a progress view anyway.

Can someone please provide a good solution for this? I am interested in knowing how Google has implemented this for their own apps (the Gmail app has it too). Are there any articles where this is shown in detail? All answers & comments will be appreciated. Thank you.

Some other references:

1. Pagination with RecyclerView. (Superb overview ...)

2. RecyclerView header and footer. (More of the same ...)

3. Endless RecyclerView with ProgressBar at bottom.

Android Solutions


Solution 1 - Android

HERE IS SIMPLER AND CLEANER APPROACH.

Implement Endless Scrolling from this Codepath Guide and then follow the following steps.

1. Add progress bar under the RecyclerView.

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_movie_grid"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:paddingBottom="50dp"
        android:clipToPadding="false"
        android:background="@android:color/black"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

    </android.support.v7.widget.RecyclerView>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="invisible"
        android:background="@android:color/transparent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

Here android:paddingBottom="50dp" and android:clipToPadding="false" are very important.

2. Get a reference to the progress bar.

progressBar = findViewById(R.id.progressBar);

3. Define methods to show and hide progress bar.

void showProgressView() {
    progressBar.setVisibility(View.VISIBLE);
}

void hideProgressView() {
    progressBar.setVisibility(View.INVISIBLE);
}

Solution 2 - Android

I implemented this on my old project, I did it as follows...

I've created an interface as the guys of your examples did

public interface LoadMoreItems {
  void LoadItems();
}

Then I add added an addOnScrollListener() on my Adapter

 recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView,
                                   int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);

                totalItemCount = linearLayoutManager.getItemCount();
                lastVisibleItem = linearLayoutManager
                        .findLastVisibleItemPosition();
                if (!loading
                        && totalItemCount <= (lastVisibleItem + visibleThreshold)) {
                    //End of the items
                    if (onLoadMoreListener != null) {
                        onLoadMoreListener.LoadItems();
                    }
                    loading = true;

                }
            }
        });

The onCreateViewHolder() is where I put the ProgressBar or not.

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent,
                                                  int viewType) {
    RecyclerView.ViewHolder vh;
    if (viewType == VIEW_ITEM) {
        View v = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.list_row, parent, false);

        vh = new StudentViewHolder(v);
    } else {
        View v = LayoutInflater.from(parent.getContext()).inflate(
                R.layout.progressbar_item, parent, false);

        vh = new ProgressViewHolder(v);
    }
    return vh;
}

On my MainActivity that is where I put the LoadItems() to add the others items is :

mAdapter.setOnLoadMoreListener(new LoadMoreItems() {
        @Override
        public void LoadItems() {
            DataItemsList.add(null);
            mAdapter.notifyItemInserted(DataItemsList.size() - 1);

            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    //   remove progress item
                    DataItemsList.remove(DataItemsList.size() - 1);
                    mAdapter.notifyItemRemoved(DataItemsList.size());
                    //add items one by one
                    //When you've added the items call the setLoaded()
                    mAdapter.setLoaded();
                    //if you put all of the items at once call
                    // mAdapter.notifyDataSetChanged();
                }
            }, 2000); //time 2 seconds

        }
    });

For more information I just followed this Github repository(Note: this is using AsyncTask maybe it's useful as my answer, since I did it manually not with data from API but it should work as well) also this post was helpful to me endless-recyclerview-with-progress-bar

Also I don't know if you named it but I also found this post infinite_scrolling_recyclerview, maybe it could also help to you.

If it's not what you are looking for, let me know and tell me what's wrong with this code and I'll try to modify it as your like.

Hope it helps.

EDIT

Since you don't want to remove an item... I found I guess one guy that removes the footer only on this post : diseño-android-endless-recyclerview.

This is for ListView but I know you can adapt it to RecyclerView he's not deleting any item he's just putting Visible/Invisible the ProgressBar take a look : detecting-end-of-listview

Also take a look to : this question android-implementing-progressbar-and-loading-for-endless-list-like-android

Solution 3 - Android

There is another way to do this.

  • First your adapter's getItemCount returns listItems.size() + 1

  • return VIEW_TYPE_LOADING in getItemViewType() for position >= listItems.size(). This way the loader will only be shown at the end of the recycler view list. The only problem with this solution is even after reaching the last page, the loader will be shown, so in order to fix that you store the x-pagination-total-count in the adapter, and

  • then you change the condition to return view type to

    (position >= listItem.size())&&(listItem.size <= xPaginationTotalCount) .

I just came up with this idea now what do you think?

Solution 4 - Android

here is my workaround without adding a fake item (in Kotlin but simple):

in your adapter add:

private var isLoading = false
private val VIEWTYPE_FORECAST = 1
private val VIEWTYPE_PROGRESS = 2

override fun getItemCount(): Int {
    if (isLoading)
        return items.size + 1
    else
        return items.size
}

override fun getItemViewType(position: Int): Int {
    if (position == items.size - 1 && isLoading)
        return VIEWTYPE_PROGRESS
    else
        return VIEWTYPE_FORECAST
}

override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
    if (viewType == VIEWTYPE_FORECAST)
        return ForecastHolder(LayoutInflater.from(context).inflate(R.layout.item_forecast, parent, false))
    else
        return ProgressHolder(LayoutInflater.from(context).inflate(R.layout.item_progress, parent, false))
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
    if (holder is ForecastHolder) {
        //init your item
    }
}

public fun showProgress() {
    isLoading = true
}


public fun hideProgress() {
    isLoading = false
}

now you can easily call showProgress() before API call. and hideProgress() after API call was done.

Solution 5 - Android

I like the idea of adding a progress view holder to an adapter, but it tends to lead to some ugly logic manipulation to get what you want. The view holder approach forces you to guard against the additional footer item by fidgeting with the return values of getItemCount(), getItemViewType(), getItemId(position) and any kind of getItem(position) method that you may want to include.

An alternative approach is to manage the ProgressBar visibility at the Fragment or Activity level by showing or hiding the ProgressBar below the RecyclerView when loading starts and ends respectively. This can be achieved by including the ProgressBar directly in the view layout or by adding it to a custom RecyclerView ViewGroup class. This solution will generally lead to less maintenance and fewer bugs.

UPDATE: My suggestion poses a problem when you scroll the view back up while the content is loading. The ProgressBar will stick to the bottom of the view layout. This is probably not the behavior you want. For this reason, adding a progress view holder to your adapter is probably the best, functional solution. Just don't forget to guard your item accessor methods. :)

Solution 6 - Android

Another possible solution is to use the ConcatAdapter available from RecyclerView 1.2.0. The drawback is that this library version is yet in alpha.

Using this approach, separate adapter is used for progress indicator, concatenated with the main adapter.

 val concatAdapter = ConcatAdapter(dataAdapter, progressAdapter)

progressAdapter should return 0 or 1 from getItemCount() method, depending on the loading state.

More info: https://medium.com/androiddevelopers/merge-adapters-sequentially-with-mergeadapter-294d2942127a

And check the current stable version of recyclerview library, might be already in a stable version at the time of reading: https://developer.android.com/jetpack/androidx/releases/recyclerview


Another viable approach would be to use recycler view item decorations. Using this approach would also save from modifying the ViewHolders. Animating the decorator is also possible, see for example: https://medium.com/mobile-app-development-publication/animating-recycler-view-decorator-9b15fa4b2c23

Basically, item decorator is added when loading indicator should be present with recyclerView.addItemDecoration() function, and then removed with recyclerView.removeItemDecoration(). Constantly invalidateItemDecorations() on the recyclerView while item decoration is shown to make animation run.


A third possibility would be to use Paging library from Google (part of Jetpack), header and footer adapters available from v3 (still in beta) https://www.youtube.com/watch?v=1cwqGOku2a4

Solution 7 - Android

This solution is inspired by Akshar Patels solution on this page. I modified it a bit.

  1. When loading the first items it looks nice to have the ProgressBar centered.

  2. I didn't like the remaining empty padding at the bottom when there existed no more items to load. That has been removed with this solution.

First the XML:

> android:id="@+id/video_list" > android:paddingBottom="60dp" > android:clipToPadding="false" > android:layout_width="match_parent" > android:layout_height="match_parent" > app:layout_constraintBottom_toBottomOf="parent" > app:layout_constraintEnd_toEndOf="parent" > app:layout_constraintStart_toStartOf="parent" > app:layout_constraintTop_toTopOf="parent"> > > > > android:id="@+id/progressBar2" > style="?android:attr/progressBarStyle" > android:layout_marginTop="10dp" > android:layout_width="wrap_content" > android:layout_centerHorizontal="true" > android:layout_height="wrap_content" > app:layout_constraintBottom_toBottomOf="parent" > app:layout_constraintEnd_toEndOf="parent" > app:layout_constraintStart_toStartOf="parent" > app:layout_constraintTop_toTopOf="parent"/>

Then I added the following programmatically.

When first results been loaded, add this to your onScrollListener. It moves the ProgressBar from center to the bottom:

ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) loadingVideos.getLayoutParams();
layoutParams.topToTop = ConstraintLayout.LayoutParams.UNSET;
loadingVideos.setLayoutParams(layoutParams);

When no more items exist, remove the padding at the bottom like this:

recyclerView.setPadding(0,0,0,0);

Hide and show your ProgressBar as usual.

Solution 8 - Android

Try this simple code :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerview_cities"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_above="@id/preogressbar"
    />
<ProgressBar
    android:id="@+id/preogressbar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="gone"
    android:layout_alignParentBottom="true"
    android:layout_centerHorizontal="true"
    />
</RelativeLayout>

Make the progress bar visible when your list items already scrolled and hide when you get data from your service.

Solution 9 - Android

You can use layout_above tag in recycleView like this:

<RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="16dp"
                    android:layout_marginRight="16dp"
                    android:orientation="vertical">
                    <androidx.recyclerview.widget.RecyclerView
                        android:id="@+id/rv"
                        android:layout_below="@+id/tv2"
                        android:layout_width="match_parent"
                        android:focusable="false"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="24dp"
                        android:layout_above="@+id/pb_pagination"/>
    
                     <ProgressBar
                        android:id="@+id/pb_pagination"
                        style="@style/Widget.AppCompat.ProgressBar"
                        android:layout_width="30dp"
                        android:layout_height="30dp"
                        android:indeterminate="true"
                         android:visibility="gone"
                        android:layout_alignParentBottom="true"
                        android:layout_centerHorizontal="true" />
    </RelativeLayout>

Solution 10 - Android

Add in Adapter

Insert a new item to the RecyclerView on a predefined position

 public void insert(int position, JobModel data) {
             joblist.add(position, data);
             notifyItemInserted(position);
        }.
    
  public void updateList(  ArrayList<JobModel> data) {
        try {
            for (int i = joblist.size(); i < data.size(); i++)
                insert(i, data.get(i));   
      }catch (Exception e){} }.

call from activity when page==2 apiSaveJobsAdapter.updateList(joblist);

Solution 11 - Android

Different approach would be to start the API call inside onBindViewHolder and initialy place into items view some progress indicator. After call is finished, you update the view (hide progress and showing received data). For example with Picasso for image loading, onBindViewHolder method would look like this

@Override
public void onBindViewHolder(final MovieViewHolder holder, final int position) {
    final Movie movie = items.get(position);
    holder.imageProgress.setVisibility(View.VISIBLE);
    Picasso.with(context)
            .load(NetworkingUtils.getMovieImageUrl(movie.getPosterPath()))
            .into(holder.movieThumbImage, new Callback() {
                @Override
                public void onSuccess() {
                    holder.imageProgress.setVisibility(View.GONE);
                }
                @Override
                public void onError() {

                }
            });
}

As I see it, there are two cases which can appear:

  1. where you download all items in light version with one call (e.g. the adapter knows immediately that he’ll have to deal with 40 pictures, but downloads it on demand —> case which I showed previously with Picasso)
  2. where you are working with real lazy loading and you are asking server to give you additional chunk of data. In this case, first prerequisite is to have adequate response from server with necessary information. Fore example { "offset": 0, "total": 100, "items": [{items}] }

There response means that you received first chunk of total 100 data. My approach would be something like this:

View After getting first chunk of data (e.g. 10) add them into adapter.

RecyclerView.Adapter.getItemCount As long as the current amount of available items is lower than total amount (e.g. available 10; total 100), in getItemCount method you will return items.size() + 1

RecyclerView.Adapter.getItemViewType if total amount of data is greater than amount of available items in adapter and the position = items.size() (i.e. you’ve fictively added item in getItemCount method), as view type you return some progress-indicator. Otherwise you’ll return normal layout type

RecyclerView.Adapter.onCreateViewHolder When you are asked to use progress-indicator view type, all you need to do is to ask your presenter to get additional chunk of items and update the adapter

So basically, this is approach where you don’t have to add/remove items from the list and where you have control over situation when lazy loading will be triggered.

Here is the code example:

public class ForecastListAdapter extends RecyclerView.Adapter<ForecastListAdapter.ForecastVH> {
private final Context context;
private List<Forecast> items;
private ILazyLoading lazyLoadingListener;

public static final int VIEW_TYPE_FIRST         = 0;
public static final int VIEW_TYPE_REST          = 1;
public static final int VIEW_TYPE_PROGRESS      = 2;

public static final int totalItemsCount = 14;

public ForecastListAdapter(List<Forecast> items, Context context, ILazyLoading lazyLoadingListener) {
    this.items = items;
    this.context = context;
    this.lazyLoadingListener = lazyLoadingListener;
}

public void addItems(List<Forecast> additionalItems){
    this.items.addAll(additionalItems);
    notifyDataSetChanged();
}

@Override
public int getItemViewType(int position) {
    if(totalItemsCount > items.size() && position == items.size()){
        return VIEW_TYPE_PROGRESS;
    }
    switch (position){
        case VIEW_TYPE_FIRST:
            return VIEW_TYPE_FIRST;
        default:
            return VIEW_TYPE_REST;
    }
}

@Override
public ForecastVH onCreateViewHolder(ViewGroup parent, int viewType) {
    View v;
    switch (viewType){
        case VIEW_TYPE_PROGRESS:
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_progress, parent, false);
            if (lazyLoadingListener != null) {
                lazyLoadingListener.getAdditionalItems();
            }
            break;
        case VIEW_TYPE_FIRST:
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_first, parent, false);
            break;
        default:
            v = LayoutInflater.from(parent.getContext()).inflate(R.layout.forecast_list_item_rest, parent, false);
            break;
    }
    return new ForecastVH(v);
}

@Override
public void onBindViewHolder(ForecastVH holder, int position) {
    if(position < items.size()){
        Forecast item = items.get(position);
        holder.date.setText(FormattingUtils.formatTimeStamp(item.getDt()));
        holder.minTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMin()));
        holder.maxTemperature.setText(FormattingUtils.getRoundedTemperature(item.getTemp().getMax()));
    }

}

@Override
public long getItemId(int position) {
    long i = super.getItemId(position);
    return i;
}

@Override
public int getItemCount() {
    if (items == null) {
        return 0;
    }
    if(items.size() < totalItemsCount){
        return items.size() + 1;
    }else{
        return items.size();
    }
}

public class ForecastVH extends RecyclerView.ViewHolder{
    @BindView(R.id.forecast_date)TextView date;
    @BindView(R.id.min_temperature)TextView minTemperature;
    @BindView(R.id.max_temperature) TextView maxTemperature;
    public ForecastVH(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);
    }
}

public interface ILazyLoading{
    public void getAdditionalItems();
}}

Maybe this'll inspire you to make something that will suit your needs

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionY.SView Question on Stackoverflow
Solution 1 - AndroidAkshar PatelView Answer on Stackoverflow
Solution 2 - AndroidSkizo-ozᴉʞSView Answer on Stackoverflow
Solution 3 - AndroidBhargavView Answer on Stackoverflow
Solution 4 - AndroidAmir ZiaratiView Answer on Stackoverflow
Solution 5 - AndroidRyanView Answer on Stackoverflow
Solution 6 - AndroidsouthertonView Answer on Stackoverflow
Solution 7 - AndroidJohn TView Answer on Stackoverflow
Solution 8 - Androidk_kumarView Answer on Stackoverflow
Solution 9 - AndroidHoussin BoullaView Answer on Stackoverflow
Solution 10 - AndroidDilip LoharView Answer on Stackoverflow
Solution 11 - AndroidTeodor HirsView Answer on Stackoverflow