📜 ⬆️ ⬇️

Pagination of lists in Android with RxJava. Part II

Good day everyone!
About a month ago, I wrote an article about organizing list pagination (RecyclerView) using RxJava. What is pagination in a simple way? This is an automatic upload of data to the list when it is scrolled.
The solution that I presented in that article was quite working, resistant to errors in answering requests for loading data, and resistant to screen reorientation (correct state preservation).
But thanks to the comments of habrovchan, their comments and suggestions, I realized that the solution has a number of shortcomings that can be completely eliminated.
Many thanks to Matthew Malkov for detailed comments and great ideas. Without it, refactoring of the past solution would not take place.
All interested in asking under the cat.

And so, what were the disadvantages of the first option:
  1. The appearance of custom AutoLoadingRecyclerView and AutoLoadingRecyclerViewAdapter . That is, just like this, this solution cannot be inserted into already written code. We'll have to work a little. And this, of course, somewhat ties hands in the future.
  2. When initializing AutoLoadingRecyclerView you must explicitly call the methods setLimit , setLoadingObservable , startLoading . And this is in addition to the standard methods for RecyclerView , such as setAdapter , setLayoutManager and others. Also in my head you need to keep that the startLoading method must be called last. Yes, all these methods are marked with comments, how and in what order they should be called, but this is not very intuitive, and you can easily get confused.
  3. The pagination mechanism was implemented in the AutoLoadingRecyclerView . A brief summary of it is as follows:
    • There is a PublishSubject with a RecyclerView.OnScrollListener , and which accordingly "emit" certain elements when an event occurs (when the user has twisted to a certain position).
    • There is a Subscriber that listens on the above-mentioned PublishSubject , and when an item arrives to it from the PublishSubject , it unsubscribes from it and calls the special Observable responsible for uploading new items.
    • And there is an Observable that loads new items, updates the list, and then reconnects Subscriber to the PublishSubject to PublishSubject for the scrolling list.

    The biggest disadvantage of this algorithm is the use of PublishSubject , which is generally recommended for use in exceptional situations and which somewhat breaks the whole concept of RxJava. As a result, we obtain several “crutch reactivity”.

Refactoring
And now, using the above disadvantages, we will try to develop a more convenient and beautiful solution.

First of all, we will get rid of PublishSubject , and for the place of it we will create Observable , which will “emit” when a given condition occurs, that is, when the user scrolls to a certain position.
The method for obtaining such an Observable (for simplicity, we will call it scrollObservable ) will be as follows:
 private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } 

Go through the parameters:
  1. RecyclerView recyclerView is our search list :)
  2. int limit - the number of loaded items at a time. I added this parameter here for the convenience of determining the "position X", after which the Observable begins to "emit". Position is determined by this expression:
     int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); 

    As I said in the last article, it was revealed in a purely empirical way, and you can change it yourself depending on the task you are solving.
  3. int emptyListCount is a more interesting parameter. Remember, I said that in the previous version, after the most recent initialization, you need to call the startLoading method for the initial load. So now, if the list is empty and it is not scrolled, then scrollObservable will automatically “emit” the first element, which is the starting point for the start of pagination:
     if (recyclerView.getAdapter().getItemCount() == 0) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } 

    But what if the list already has some elements "by default" (for example, one element). And pagination must somehow begin. This is precisely emptyListCount parameter helps emptyListCount .
     int emptyListCount = 1; if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } 


The resulting scrollObservable "emit" number equal to the number of items in the list. This number is also a shift (or “offset”).
 subscriber.onNext(recyclerView.getAdapter().getItemCount()); 

When scrolling after reaching a certain position, scrollObservable starts to "emit" elements en masse. We need only one “emit” with the changed “offset”. Therefore, we add the distinctUntilChanged() operator, which cuts off all repeating elements.
Code:
 getScrollObservable(recyclerView, limit, emptyListCount) .distinctUntilChanged(); 

It is also necessary to remember that we work with the UI element and monitor changes in its state. Therefore, all the work on “tapping” the scrolling of the list should occur in the UI stream:
 getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged(); 


Now it is necessary to properly load this data.
To do this, create the PagingListener interface, implementing which, the developer sets the Observable , which is responsible for loading data:
 public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } 

Switching to the “loading” Observable accomplished using the switchMap operator. Also remember that it is desirable to load data in a non-UI stream.
Attention to the code:
 getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(pagingListener::onNextPage); 

We subscribe to this Observable already in the fragment or activation, where the developer decides how to deal with newly uploaded data. Or they are immediately in the list, or filtered, and only then the list. The great thing is that we can easily upgrade the Observable way we want. In this, of course, RxJava is wonderful, and the Subject that was in the last article is not an assistant.
')
Error processing
But what if there was a short-term error when loading data, such as “network lost”, etc.? We must be able to re-try to query the data. Of course, the retry(long count) operator retry() I avoid the retry() operator because of the possibility of a hangup if the error is not short-lived). Then:
 getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(pagingListener::onNextPage) .retry(3); 

But that's the problem. If an error occurs and the user reaches the end of the list, nothing will happen, the second request will not be sent. The thing is that the operator retry(long count) in case of an error re-subscribes Subscriber to the Observable , and we again "listen" to the scrolling list. And the list has reached the end, so a second request does not occur. It is treated only by “twitching” the list to make scrolling work. But this, of course, is not correct.

Therefore, we had to dodge so that in case of an error the request would still be sent again regardless of scrolling the list and no more than the number of times that the developer would ask.
The solution is:
 int startNumberOfRetryAttempt = 0; getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)) private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } 

The retryCount parameter retryCount set by the developer. This is the maximum number of repeated requests in case of an error. That is, this is not the maximum number of attempts for all requests, but the maximum - only for a specific request.
How does this code work, or rather the getPagingObservable method?
We Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
to the Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
 Observable<List> observable   onErrorResumeNext ,       Observable .          .     retryCount : 
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .
Observable<List> observable onErrorResumeNext , Observable . . retryCount :
if (numberOfAttemptToRetry < retryCount) {
, :
int attemptToRetryInc = numberOfAttemptToRetry + 1;
, , listener.onNextPage(offset) :
return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount);
, Observable :
return Observable.empty();


PaginationTool .
PaginationTool /** * @author e.matsyuk */ public class PaginationTool { // for first start of items loading then on RecyclerView there are not items and no scrolling private static final int EMPTY_LIST_ITEMS_COUNT = 0; // default limit for requests private static final int DEFAULT_LIMIT = 50; // default max attempts to retry loading request private static final int MAX_ATTEMPTS_TO_RETRY_LOADING = 3; public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener) { return paging(recyclerView, pagingListener, DEFAULT_LIMIT, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit) { return paging(recyclerView, pagingListener, limit, EMPTY_LIST_ITEMS_COUNT, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount) { return paging(recyclerView, pagingListener, limit, emptyListCount, MAX_ATTEMPTS_TO_RETRY_LOADING); } public static <T> Observable<List<T>> paging(RecyclerView recyclerView, PagingListener<T> pagingListener, int limit, int emptyListCount, int retryCount) { if (recyclerView == null) { throw new PagingException("null recyclerView"); } if (recyclerView.getAdapter() == null) { throw new PagingException("null recyclerView adapter"); } if (limit <= 0) { throw new PagingException("limit must be greater then 0"); } if (emptyListCount < 0) { throw new PagingException("emptyListCount must be not less then 0"); } if (retryCount < 0) { throw new PagingException("retryCount must be not less then 0"); } int startNumberOfRetryAttempt = 0; return getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(offset -> getPagingObservable(pagingListener, pagingListener.onNextPage(offset), startNumberOfRetryAttempt, offset, retryCount)); } private static Observable<Integer> getScrollObservable(RecyclerView recyclerView, int limit, int emptyListCount) { return Observable.create(subscriber -> { final RecyclerView.OnScrollListener sl = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (!subscriber.isUnsubscribed()) { int position = getLastVisibleItemPosition(recyclerView); int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } } } }; recyclerView.addOnScrollListener(sl); subscriber.add(Subscriptions.create(() -> recyclerView.removeOnScrollListener(sl))); if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); } }); } private static int getLastVisibleItemPosition(RecyclerView recyclerView) { Class recyclerViewLMClass = recyclerView.getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)recyclerView.getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)recyclerView.getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new PagingException("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } private static <T> Observable<List<T>> getPagingObservable(PagingListener<T> listener, Observable<List<T>> observable, int numberOfAttemptToRetry, int offset, int retryCount) { return observable.onErrorResumeNext(throwable -> { // retry to load new data portion if error occurred if (numberOfAttemptToRetry < retryCount) { int attemptToRetryInc = numberOfAttemptToRetry + 1; return getPagingObservable(listener, listener.onNextPage(offset), attemptToRetryInc, offset, retryCount); } else { return Observable.empty(); } }); } } PagingException /** * @author e.matsyuk */ public class PagingException extends RuntimeException { public PagingException(String detailMessage) { super(detailMessage); } } PagingListener /** * @author e.matsyuk */ public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); } PaginationFragment /** * A placeholder fragment containing a simple view. */ public class PaginationFragment extends Fragment { private final static int LIMIT = 50; private PagingRecyclerViewAdapter recyclerViewAdapter; private Subscription pagingSubscription; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fmt_pagination, container, false); setRetainInstance(true); init(rootView, savedInstanceState); return rootView; } @Override public void onResume() { super.onResume(); } private void init(View view, Bundle savedInstanceState) { RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); // init adapter for the first time if (savedInstanceState == null) { recyclerViewAdapter = new PagingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); } recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setAdapter(recyclerViewAdapter); // if all items was loaded we don't need Pagination if (recyclerViewAdapter.isAllItemsLoaded()) { return; } // RecyclerView pagination pagingSubscription = PaginationTool .paging(recyclerView, offset -> EmulateResponseManager.getInstance().getEmulateResponse(offset, LIMIT), LIMIT) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<List<Item>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { } @Override public void onNext(List<Item> items) { recyclerViewAdapter.addNewItems(items); recyclerViewAdapter.notifyItemInserted(recyclerViewAdapter.getItemCount() - items.size()); } }); } @Override public void onDestroyView() { if (pagingSubscription != null && !pagingSubscription.isUnsubscribed()) { pagingSubscription.unsubscribe(); } super.onDestroyView(); } } PagingRecyclerViewAdapter /** * @author e.matsyuk */ public class PagingRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private static final int MAIN_VIEW = 0; private List<Item> listElements = new ArrayList<>(); // after reorientation test this member // or one extra request will be sent after each reorientation private boolean allItemsLoaded; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } public void addNewItems(List<Item> items) { if (items.size() == 0) { allItemsLoaded = true; return; } listElements.addAll(items); } public boolean isAllItemsLoaded() { return allItemsLoaded; } @Override public long getItemId(int position) { return getItem(position).getId(); } public Item getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == MAIN_VIEW) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false); return new MainViewHolder(v); } return null; } @Override public int getItemViewType(int position) { return MAIN_VIEW; } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { switch (getItemViewType(position)) { case MAIN_VIEW: onBindTextHolder(holder, position); break; } } private void onBindTextHolder(RecyclerView.ViewHolder holder, int position) { MainViewHolder mainHolder = (MainViewHolder) holder; mainHolder.textView.setText(getItem(position).getItemStr()); } }
GitHub .

! , , , .

Source: https://habr.com/ru/post/271875/


All Articles