Endless RecyclerView with ProgressBar for pagination
E

11

66

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.

Embower answered 23/12, 2015 at 8:47 Comment(10)
OK - i asked question 2 u referenced to and till now I cannot find a better solution to what was proposed by Vilen. I have also used his solution in a few activities containing recyclerview with no problems at all. As far as I know there are no built in support for progressBar in a recyclerview as google built it as a vanilla option for developers with no built in bells and whistles so if you want bells and whistles you do need to built them into the recyclerview yourself. Is there a reason why you are doubting Vilen's code and has there been instances where it has not worked for you?Letti
Fine, I will explain in more detail ... using notifyItemInserted() and notifyItemRemoved() while the user is scrolling leads to some lagging and jarring effects in the list. This always happens with this approach, especially when the user flings the list. Google's own apps show the spinner in their paginated lists too, but without any kind of lagging or shifting of the list items. I conclude that they have used some other approach that does not involve adding a null object into the data set midway while the user is scrolling ... so I am interested in their solution ... :)Embower
So the 'null' object used is a bit of a problem - i do not use it, I added an extra field called 'viewType' within my object arrayList and set its value to 'progressBar'. In my adapter's 'getItemViewType' method, I retrieve the object's 'viewType' and then use it to determine which view I should display, whether it is a normal recyclerview view or a progressBar. This helps prevent nullpointerexceptions. I haven't experienced any lagging or jagging effects when scrolling. If you want to test this out yourself, try this repo I have made: github.com/Winghin2517/ReverseLayoutRecyclerviewLetti
The repo is for a reverseLayoutRecyclerview with his code inside but is an inverted recyclerview that only contains 3 items inside the recyclerview. You can uncomment out the portion of the code where it will add 15 more items if you scroll up to make it more functional.Letti
Thank you for your code ... I will try it out ... :)Embower
if you don't want to hack the adapter, let getItemCount return the real size + 1 while you are waiting for the next page, you could try adding the spinner below the RecyclerView in the layout and change its visibility accordingly. I use the second approach let the spinner overlap the latest visible item (with a semitransparent background)Triquetrous
@Blackbelt: that sounds like an interesting approach, Blackbelt ... let me try it out and see if works ... :)Embower
Could someone ask CommansWare to answer this question I bet he has a neat solution for this problemAirliner
i tried this approach also. doesnt look the best!! might try it inside a snack bar if thats even possible.Sashasashay
so finally what is the solution you chose?Paleogeography
D
36

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);
}
Dorkas answered 14/11, 2017 at 16:17 Comment(4)
how to remove bottom padding when there is no more data to load. It shows a space of 50dp and it seems very weirdAntinomy
@BhargavThanki you can remove the padding programmatically by getting the instance of RecyclerView. Remove the padding in hideProgressView() and Add it in showProgressView().Dorkas
This implementation has few drawbacks it doesn't show the loader when items are in the full screen of mobile, so you better go with the standard style & defining the different viewtype for pagination in your viewholder.Kincaid
Use View.GONE , it will put the progress invisible and also constrain your recyclerview to the bottomKnighthood
N
15

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

Noncooperation answered 27/12, 2015 at 20:16 Comment(14)
Thank you .... this is the approach that I am trying to avoid, as I don't want to add and remove items while the scroll is in progress ... :)Embower
But you only remove the progress that count as an item, have you tried it?Absurdity
Yes ... We add and remove the null object ... I am trying to avoid that as it causes the list to jitter ... Google's own apps don't seem to use this approach ... :)Embower
Sorry for replying late, Skizo ... I am studying those links you provided ... :)Embower
Don't worry master, if you need me to improve the answer or something let me know and I'll be glad to do it :) or if you want I can put a bounty on this question to see if someone with more knowledge can solve your problem :DAbsurdity
Thanks for your efforts ... I am working to find a better solution to this ... :)Embower
@Y.S. Did you find any better? If you did, feel free to share with me if you want.. I would like to know how to do it aswell more efficient :PAbsurdity
I did go through the links you provided ... Blackbelt's suggestion is also interesting ... I am still working on a proper solution ... Will update once I finish ... Feel free to change / edit any part of this post as you wish ... :)Embower
Still no perfect solution ... :)Embower
I am also looking for the same.. But after reading these comments, it's like no luck!Slit
Very Clear AnswerAldose
@DevTamil Thanks :DAbsurdity
@Y.S. Did you find any better solution for it?Pretermit
And when loading = false?Sedberry
A
5

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?

Airliner answered 12/1, 2016 at 9:35 Comment(4)
does it work properly ? without lagging & distortion ?Embower
why would there be any distortion there is like 2 condition checks for every get item view type the draw backs of this logic isnt efficiency, but in scenarios where network request for the next page fails then the loader icon would still be shown at the end of the listAirliner
Thanks for your efforts ... I'll try this out ... :)Embower
I already did this, but without the pagination max count check and the fallback when network request fails >.>, anyways its smooth no distortion or anythingAirliner
T
4

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.

Torry answered 27/4, 2018 at 9:27 Comment(0)
H
3

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. :)

Hospitium answered 6/4, 2016 at 17:50 Comment(2)
Yes that is also fine, I believe. It's similar to the header and footer concept in ListView, right?Embower
@Y.S. I updated my answer with a code snippet to clarify my suggestion.Hospitium
I
3

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

Indigested answered 30/3, 2021 at 16:53 Comment(0)
P
2

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.support.v7.widget.RecyclerView
    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.support.v7.widget.RecyclerView>

<ProgressBar
    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.

Pecos answered 21/6, 2018 at 10:6 Comment(0)
U
1

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.

Uniformed answered 13/7, 2020 at 18:27 Comment(0)
A
0

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>
Amphiboly answered 23/7, 2019 at 14:37 Comment(0)
I
0

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);

Idaidae answered 6/2, 2021 at 15:55 Comment(0)
F
-3

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

Faustina answered 13/10, 2016 at 11:37 Comment(4)
Making a web service / API call inside every item of the collection? Really? Sounds tedious and highly inefficient / expensive. Thank you for your answer though ... :)Embower
do you need also view part?Faustina
just out of curiosity - did you give it a shoot? I'd be glad to receive also positive feedback if it helpsFaustina
@Y.S. Picasso is pretty good at caching images, so the image is loaded from network once and reused on subsequent onBindViewHolder calls for that image.Revolution

© 2022 - 2024 — McMap. All rights reserved.