約1か月前、RxJavaを使用してリストのページネーション(RecyclerView)を整理することに関する記事を書きました 。 ページネーションは簡単ですか? これは、リストがスクロールされるときにリストにデータを自動的にロードします。
この記事で紹介した解決策は非常に効果的であり、データの読み込み要求に対する回答のエラーに耐性があり、画面の向きの変更(正しい状態の保存)に耐性がありました。
しかし、ハブロフスク市民のコメント、彼らのコメントと提案のおかげで、私はこのソリューションには完全に排除できる多くの欠点があることに気付きました。
詳細なコメントと素晴らしいアイデアをいただいたMatvey Malkovに感謝します。 それがなければ、過去の決定のリファクタリングは行われなかっただろう。
猫に興味がある人は誰でも聞いてください。
したがって、最初のオプションの欠点は何でしたか?
- カスタム
AutoLoadingRecyclerView
およびAutoLoadingRecyclerViewAdapter
の出現。 つまり、そのように、既に作成されたコードにこのソリューションを挿入することはできません。 少し働かなければならない。 そして、これはもちろん、将来的にいくらか手を縛ります。 -
setLimit
を初期化するとき、setLimit
、setLoadingObservable
、startLoading
明示的に呼び出す必要がありstartLoading
。 また、これはsetAdapter
、setLayoutManager
などのRecyclerView
標準メソッドに追加されます。 また、startLoading
メソッドは最後にstartLoading
必要があることにstartLoading
してstartLoading
。 はい、これらすべてのメソッドには、どのように、どの順序で呼び出されるべきかについてのコメントが付いていますが、これはあまり直感的ではなく、簡単に混乱してしまいます。 - ページネーションメカニズムは
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()); } }); }
パラメーターを見てみましょう。
-
RecyclerView recyclerView
検索リスト:) -
int limit
一度にロードされるアイテムの数。 「X位置」を決定する便宜上、ここにこのパラメーターを追加しました。その後、Observable
は「放出」を開始します。 位置は次の式によって決定されます。
int updatePosition = recyclerView.getAdapter().getItemCount() - 1 - (limit / 2);
前の記事で述べたように、これは経験的に明らかにされており、あなたが解決しようとしている問題に応じて、すでに自分で変更することができます。
-
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 .
! , , , .