RxJavaを使用してAndroidのページネーションをリストします。 パートII

すべての人に良い一日を!

約1か月前、RxJavaを使用してリストのページネーション(RecyclerView)を整理することに関する記事を書きまし 。 ページネーションは簡単ですか? これは、リストがスクロールされるときにリストにデータを自動的にロードします。

この記事で紹介した解決策は非常に効果的であり、データの読み込み要求に対する回答のエラーに耐性があり、画面の向きの変更(正しい状態の保存)に耐性がありました。

しかし、ハブロフスク市民のコメント、彼らのコメントと提案のおかげで、私はこのソリューションには完全に排除できる多くの欠点があることに気付きました。

詳細なコメントと素晴らしいアイデアをいただいたMatvey Malkovに感謝します。 それがなければ、過去の決定のリファクタリングは行われなかっただろう。

猫に興味がある人は誰でも聞いてください。



したがって、最初のオプションの欠点は何でしたか?

  1. カスタムAutoLoadingRecyclerView



    およびAutoLoadingRecyclerViewAdapter



    の出現。 つまり、そのように、既に作成されたコードにこのソリューションを挿入することはできません。 少し働かなければならない。 そして、これはもちろん、将来的にいくらか手を縛ります。
  2. setLimit



    を初期化するとき、 setLimit



    setLoadingObservable



    startLoading



    明示的に呼び出す必要がありstartLoading



    。 また、これはsetAdapter



    setLayoutManager



    などのRecyclerView



    標準メソッドに追加されます。 また、 startLoading



    メソッドは最後にstartLoading



    必要があることにstartLoading



    してstartLoading



    。 はい、これらすべてのメソッドには、どのように、どの順序で呼び出されるべきかについてのコメントが付いていますが、これはあまり直感的ではなく、簡単に混乱してしまいます。
  3. ページネーションメカニズムはAutoLoadingRecyclerView



    で実装されました。 その概要は次のとおりです。

    • RecyclerView.OnScrollListener



      にアタッチされているPublishSubject



      があり、イベントが発生したとき(ユーザーが特定の位置にねじ込んだとき)に特定の要素を「放出」します。
    • 前述のPublishSubject



      をリッスンするSubscriber



      があり、 PublishSubject



      持つ要素PublishSubject



      すると、サブスクPublishSubject



      解除し、新しい要素の読み込みを担当する特別Observable



      を呼び出します。
    • そしてObservable



      があり、新しい要素をロードし、リストを更新してから、 Subscriber



      PublishSubject



      に再度接続して、リストのスクロールをPublishSubject



      します。


    このアルゴリズムの最大の欠点はPublishSubject



    の使用です。これは通常、例外的な状況での使用が推奨されており、RxJavaの概念全体をやや破ります。 その結果、いくつかの「松葉杖反応」を取得します。



リファクタリング

そして今、上記の欠点を利用して、より便利で美しいソリューションを開発しようとします。



まず、 PublishSubject



、その場所のObservable



を作成します。これは、指定された条件が発生したとき、つまりユーザーが特定の位置にスクロールしたときに「発光」します。

そのようなObservable



を取得するためのメソッド(簡単にするため、 scrollObservable



と呼びます)は次のようになります。

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





パラメーターを見てみましょう。

  1. RecyclerView recyclerView



    検索リスト:)
  2. int limit



    一度にロードされるアイテムの数。 「X位置」を決定する便宜上、ここにこのパラメーターを追加しました。その後、 Observable



    は「放出」を開始します。 位置は次の式によって決定されます。

     int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2);
          
          





    前の記事で述べたように、これは経験的に明らかにされており、あなたが解決しようとしている問題に応じて、すでに自分で変更することができます。

  3. int emptyListCount



    はすでにより興味深いパラメーターです。 以前のバージョンでは、最新バージョンで初期化した後、初期ロードのためにstartLoading



    メソッドを呼び出す必要があることをstartLoading



    。 そのため、リストが空でスクロールできない場合、 scrollObservable



    は最初の要素を自動的に「放出」します。これは、改ページの開始の開始点として機能します。

     if (recyclerView.getAdapter().getItemCount() == 0) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); }
          
          





    しかし、リストに既にいくつかの要素が「デフォルトで」ある場合(たとえば、1つの要素)。 しかし、ページネーションは何とかして始めなければなりません。 これはまさにemptyListCount



    パラメーターがemptyListCount





     int emptyListCount = 1; if (recyclerView.getAdapter().getItemCount() == emptyListCount) { subscriber.onNext(recyclerView.getAdapter().getItemCount()); }
          
          







結果のscrollObservable



は、リスト内の要素の数に等しい数を「放出」します。 同じ番号がシフト(または「オフセット」)です。

 subscriber.onNext(recyclerView.getAdapter().getItemCount());
      
      





特定の位置に達した後にスクロールすると、 scrollObservable



は要素の大量放出を開始します。 「オフセット」が変更された「エミット」が1つだけ必要です。 したがって、 distinctUntilChanged()



演算子を追加します。これは、すべての繰り返し要素を切り離します。

コード:

 getScrollObservable(recyclerView, limit, emptyListCount) .distinctUntilChanged();
      
      





UI要素を操作し、その状態の変化を追跡することも覚えておく必要があります。 したがって、リストをスクロールするための「リスニング」のすべての作業は、UIスレッドで行われる必要があります。

 getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged();
      
      







ここで、このデータを正しくロードする必要があります。

これを行うには、 PagingListener



インターフェースを作成し、実装します。開発者は、データのロードを担当するObservable



設定します。

 public interface PagingListener<T> { Observable<List<T>> onNextPage(int offset); }
      
      





switchMap



オペレーターを使用すると、「ロード」 Observable



への切り替えがswitchMap



です。 また、UIストリームではなくデータの読み込みを実行することが望ましいことを覚えておいてください。

コードへの注意:

 getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(pagingListener::onNextPage);
      
      





開発者が新しくロードされたデータの処理方法を決定するフラグメントまたはアクティビティで既にこのObservable



サブスクライブします。 または、すぐにリストに入れるか、またはフィルターしてから、リストのみにします。 すばらしいことは、 Observable



を望みObservable



簡単に構築できることです。 もちろん、RxJavaは素晴らしいものであり、前回の記事にあったSubject



はアシスタントではありません。



エラー処理

しかし、「ネットワークが失われた」など、データの読み込み中に短期的なエラーが発生した場合はどうなりますか? データの要求を再試行できるはずです。 もちろん、 retry(long count)



演算子がretry()



演算子は、エラーが短期的でない場合にフリーズする可能性があるため回避します)。 次に:

 getScrollObservable(recyclerView, limit, emptyListCount) .subscribeOn(AndroidSchedulers.mainThread()) .distinctUntilChanged() .observeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .switchMap(pagingListener::onNextPage) .retry(3);
      
      





しかし、ここに問題があります。 エラーが発生し、ユーザーがリストの最後までスクロールした場合、何も起こりません。2番目のリクエストは送信されません。 問題は、エラーの場合にretry(long count)



演算子がSubscriber



Observable



に再署名し、再びリストのスクロールを「リッスン」することです。 そして、リストが最後に達したため、2番目の要求は発生しません。 スクロールが機能するように、リストを「ぴくぴく」させるだけで処理されます。 しかし、これはもちろん正しくありません。



したがって、エラーが発生した場合に、リストのスクロールに関係なく、開発者が要求したものだけでリクエストが再送されるように、回避する必要がありました。

解決策は次のとおりです。

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





retryCount



パラメーターretryCount



、開発者retryCount



設定します。 これは、エラーが発生した場合の最大再試行回数です。 つまり、これはすべてのリクエストの最大試行回数ではなく、特定のリクエストのみの最大回数です。

このコードはどのように機能しますか、 getPagingObservable



メソッドですか?

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 .



! , , , .







All Articles