📜 ⬆️ ⬇️

Pagination of lists in Android with RxJava. Part I

Often when developing a client, we are faced with the task of displaying any information from a server, database or something else in the form of a list. And when scrolling through the list, the data should automatically be loaded and inserted into the list unnoticed by the user. The user should generally get the impression that he will scroll an endless list.

In this article I would like to tell you how to make an auto-loadable list the simplest to implement for a developer and as efficient and fast as possible for a user. And also how RxJava will help us in this with its main dogma - “Everything is Stream!”

How do we implement this in Android?


To begin with, we will define the source data:
  1. The component RecyclerView is responsible for displaying the list (I hope, we have already forgotten everything about ListView :)) and all the necessary classes for setting up the RecyclerView .
  2. The data will be loaded using queries (to the network, to the database, etc.) with the parameters classical for this task offset and limit

Next, we describe the approximate algorithm of the auto-powered list:
  1. We load the first portion of data for the list. We display this data.
  2. When scrolling the list, we need to keep track of which elements are displayed on the screen by number. Specifically, the sequence number of the first or last member visible to the user.
  3. When an event occurs, for example, the last element visible on the screen is the last one in the list in general, we need to load a new piece of data. It is also necessary to prevent the sending of identical requests. That is, you need to somehow unsubscribe from "listening" to scrolling the list.
  4. New data send to the list. The list must be updated. Subscribe to the scroll hitch again.
  5. Items 2, 3, 4 must be repeated until all the data have been downloaded, or when another event is necessary.

And what does RxJava have to do with it?

Remember, I spoke at the beginning about the main dogma Rx - “Everything is Stream!”. If in OOP we think in categories of objects, in Reactive - in categories of flows.
')
For example, take a look at the second paragraph of the algorithm. The first thing we’ll stop at here is the scrolling and, accordingly, the changing sequence number of the first or last item visible on the screen (in the example we are considering, the last one). That is, the list when scrolling constantly "radiates" the numbers of the last element throughout its life. Nothing like? Of course, this is the classic “hot observable”. And to be more specific, this is PublishSubject . Secondly, Subscriber will perfectly suit the role of a “listener”.

RxJava is like a huge set of "designers" from which the developer can create a variety of designs. The beginning of the construction has already been made - the list "releases" the elements that are the numbers of the last visible line in the list. Next, you can insert a constructor (they are essentially the same as streams), which are responsible for processing the received values, sending requests for uploading new data and embedding them in the list. Well, first things first.

Give a reactive code!

And now to the practical part.

Since we are creating a new auto-loadable list, we’ll inherit from RecyclerView .

 public class AutoLoadingRecyclerView<T> extends RecyclerView 

Next, we must set the list parameter limit , responsible for the size of the portion of the loaded data at a time.

 private int limit; public int getLimit() { if (limit <= 0) { throw new AutoLoadingRecyclerViewExceptions("limit must be initialised! And limit must be more than zero!"); } return limit; } /** * required method */ public void setLimit(int limit) { this.limit = limit; } 

Now AutoLoadingRecyclerView should "radiate" the sequence number of the last element visible on the screen.

However, the “emission” of just the sequence number is not very convenient in the future. After all, this value needs to be processed. Yes, and our channel (also known as the “emitter”) will flood considerably, which also imposes a problem on the backpressure. Then we will improve a little "radiator". Let the output we will receive immediately ready-made values ​​of offset and limit , combined into the following model:
 public class OffsetAndLimit { private int offset; private int limit; public OffsetAndLimit(int offset, int limit) { this.offset = offset; this.limit = limit; } public int getOffset() { return offset; } public int getLimit() { return limit; } } 

Already better. And now reduce the "flood" channel. Let the channel “radiate” elements only when necessary, that is, when you need to load a new portion of data.

Take a look at the code.

 private PublishSubject<OffsetAndLimit> scrollLoadingChannel = PublishSubject.create(); //    private void startScrollingChannel() { addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { int position = getLastVisibleItemPosition(); int limit = getLimit(); int updatePosition = getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { int offset = getAdapter().getItemCount() - 1; OffsetAndLimit offsetAndLimit = new OffsetAndLimit(offset, limit); scrollLoadingChannel.onNext(offsetAndLimit); } } }); } //          //     LayoutManager private int getLastVisibleItemPosition() { Class recyclerViewLMClass = getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new AutoLoadingRecyclerViewExceptions("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } 

You probably want to ask where I got this condition from:

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

It was revealed in a purely empirical way, assuming that the average request time is 200-300ms. Under this condition, “smooth scrolling” does not suffer from parallel loading of data. If your request time is longer, you can either try to increase the limit or slightly change this condition so that the data loading takes place a little earlier.

But still, we didn’t get rid of the channel completely. When the condition of the start of loading is fulfilled and scrolling continues, the channel continues to “flood” us with messages. And we have all the possibilities to send the same requests for data uploading several times, and nobody backdated them - the network client may break. Therefore, as soon as we receive the first message from the channel, we immediately unsubscribe from it, start loading the data, update the adapter and the list, and then subscribe again to the channel that will no longer “flood” as the condition changes (the number of elements in list will increase):

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

And so on the cycle. And now attention to the code:

 //     private void subscribeToLoadingChannel() { Subscriber<OffsetAndLimit> toLoadingChannelSubscriber = new Subscriber<OffsetAndLimit>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { Log.e(TAG, "subscribeToLoadingChannel error", e); } @Override public void onNext(OffsetAndLimit offsetAndLimit) { //    unsubscribe(); //    loadNewItems(offsetAndLimit); } }; // scrollLoadingChannel -   .    subscribeToLoadingChannelSubscription = scrollLoadingChannel .subscribe(toLoadingChannelSubscriber); } //    private void loadNewItems(OffsetAndLimit offsetAndLimit) { Subscriber<List<T>> loadNewItemsSubscriber = new Subscriber<List<T>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { Log.e(TAG, "loadNewItems error", e); subscribeToLoadingChannel(); } @Override public void onNext(List<T> ts) { //      //  ,      addNewItems.    ,    getAdapter().addNewItems(ts); //   getAdapter().notifyItemInserted(getAdapter().getItemCount() - ts.size()); //        ,      (),      . //        if (ts.size() > 0) { //     subscribeToLoadingChannel(); } } }; // getLoadingObservable().getLoadingObservable(offsetAndLimit) -     AutoLoadingRecyclerView Observable.     loadNewItemsSubscription = getLoadingObservable().getLoadingObservable(offsetAndLimit) //     UI  .subscribeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) //     UI (       ) //      (         ), //   View   UI  .observeOn(AndroidSchedulers.mainThread()) .subscribe(loadNewItemsSubscriber); } 

The hardest thing behind. We managed to organize a safe list update cycle. And all this inside our AutoLoadingRecyclerView .

And in order for us to have a holistic impression, focus on the full code below:

OffsetAndLimit
 /** * Offset and limit for {@link AutoLoadingRecyclerView AutoLoadedRecyclerView channel} * * @author e.matsyuk */ public class OffsetAndLimit { private int offset; private int limit; public OffsetAndLimit(int offset, int limit) { this.offset = offset; this.limit = limit; } public int getOffset() { return offset; } public int getLimit() { return limit; } @Override public String toString() { return "OffsetAndLimit{" + "offset=" + offset + ", limit=" + limit + '}'; } } 


AutoLoadingRecyclerViewExceptions
 /** * @author e.matsyuk */ public class AutoLoadingRecyclerViewExceptions extends RuntimeException { public AutoLoadingRecyclerViewExceptions() { super("Exception in AutoLoadingRecyclerView"); } public AutoLoadingRecyclerViewExceptions(String detailMessage) { super(detailMessage); } } 


Ioading
 /** * @author e.matsyuk */ public interface ILoading<T> { Observable<List<T>> getLoadingObservable(OffsetAndLimit offsetAndLimit); } 


AutoLoadingRecyclerViewAdapter
 /** * Adapter for {@link AutoLoadingRecyclerView AutoLoadingRecyclerView} * * @author e.matsyuk */ public abstract class AutoLoadingRecyclerViewAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> { private List<T> listElements = new ArrayList<>(); public void addNewItems(List<T> items) { listElements.addAll(items); } public List<T> getItems() { return listElements; } public T getItem(int position) { return listElements.get(position); } @Override public int getItemCount() { return listElements.size(); } } 


LoadingRecyclerViewAdapter
 /** * @author e.matsyuk */ public class LoadingRecyclerViewAdapter extends AutoLoadingRecyclerViewAdapter<Item> { private static final int MAIN_VIEW = 0; static class MainViewHolder extends RecyclerView.ViewHolder { TextView textView; public MainViewHolder(View itemView) { super(itemView); textView = (TextView) itemView.findViewById(R.id.text); } } @Override public long getItemId(int position) { return getItem(position).getId(); } @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()); } } 


AutoLoadingRecyclerView
 /** * @author e.matsyuk */ public class AutoLoadingRecyclerView<T> extends RecyclerView { private static final String TAG = "AutoLoadingRecyclerView"; private static final int START_OFFSET = 0; private PublishSubject<OffsetAndLimit> scrollLoadingChannel = PublishSubject.create(); private Subscription loadNewItemsSubscription; private Subscription subscribeToLoadingChannelSubscription; private int limit; private ILoading<T> iLoading; private AutoLoadingRecyclerViewAdapter<T> autoLoadingRecyclerViewAdapter; public AutoLoadingRecyclerView(Context context) { super(context); init(); } public AutoLoadingRecyclerView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public AutoLoadingRecyclerView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } /** * required method * call after init all parameters in AutoLoadedRecyclerView */ public void startLoading() { OffsetAndLimit offsetAndLimit = new OffsetAndLimit(START_OFFSET, getLimit()); loadNewItems(offsetAndLimit); } private void init() { startScrollingChannel(); } private void startScrollingChannel() { addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { int position = getLastVisibleItemPosition(); int limit = getLimit(); int updatePosition = getAdapter().getItemCount() - 1 - (limit / 2); if (position >= updatePosition) { int offset = getAdapter().getItemCount() - 1; OffsetAndLimit offsetAndLimit = new OffsetAndLimit(offset, limit); scrollLoadingChannel.onNext(offsetAndLimit); } } }); } private int getLastVisibleItemPosition() { Class recyclerViewLMClass = getLayoutManager().getClass(); if (recyclerViewLMClass == LinearLayoutManager.class || LinearLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { LinearLayoutManager linearLayoutManager = (LinearLayoutManager)getLayoutManager(); return linearLayoutManager.findLastVisibleItemPosition(); } else if (recyclerViewLMClass == StaggeredGridLayoutManager.class || StaggeredGridLayoutManager.class.isAssignableFrom(recyclerViewLMClass)) { StaggeredGridLayoutManager staggeredGridLayoutManager = (StaggeredGridLayoutManager)getLayoutManager(); int[] into = staggeredGridLayoutManager.findLastVisibleItemPositions(null); List<Integer> intoList = new ArrayList<>(); for (int i : into) { intoList.add(i); } return Collections.max(intoList); } throw new AutoLoadingRecyclerViewExceptions("Unknown LayoutManager class: " + recyclerViewLMClass.toString()); } public int getLimit() { if (limit <= 0) { throw new AutoLoadingRecyclerViewExceptions("limit must be initialised! And limit must be more than zero!"); } return limit; } /** * required method */ public void setLimit(int limit) { this.limit = limit; } @Deprecated @Override public void setAdapter(Adapter adapter) { if (adapter instanceof AutoLoadingRecyclerViewAdapter) { super.setAdapter(adapter); } else { throw new AutoLoadingRecyclerViewExceptions("Adapter must be implement IAutoLoadedAdapter"); } } /** * required method */ public void setAdapter(AutoLoadingRecyclerViewAdapter<T> autoLoadingRecyclerViewAdapter) { if (autoLoadingRecyclerViewAdapter == null) { throw new AutoLoadingRecyclerViewExceptions("Null adapter. Please initialise adapter!"); } this.autoLoadingRecyclerViewAdapter = autoLoadingRecyclerViewAdapter; super.setAdapter(autoLoadingRecyclerViewAdapter); } public AutoLoadingRecyclerViewAdapter<T> getAdapter() { if (autoLoadingRecyclerViewAdapter == null) { throw new AutoLoadingRecyclerViewExceptions("Null adapter. Please initialise adapter!"); } return autoLoadingRecyclerViewAdapter; } public void setLoadingObservable(ILoading<T> iLoading) { this.iLoading = iLoading; } public ILoading<T> getLoadingObservable() { if (iLoading == null) { throw new AutoLoadingRecyclerViewExceptions("Null LoadingObservable. Please initialise LoadingObservable!"); } return iLoading; } private void subscribeToLoadingChannel() { Subscriber<OffsetAndLimit> toLoadingChannelSubscriber = new Subscriber<OffsetAndLimit>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { Log.e(TAG, "subscribeToLoadingChannel error", e); } @Override public void onNext(OffsetAndLimit offsetAndLimit) { unsubscribe(); loadNewItems(offsetAndLimit); } }; subscribeToLoadingChannelSubscription = scrollLoadingChannel .subscribeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .observeOn(AndroidSchedulers.mainThread()) .subscribe(toLoadingChannelSubscriber); } private void loadNewItems(OffsetAndLimit offsetAndLimit) { Subscriber<List<T>> loadNewItemsSubscriber = new Subscriber<List<T>>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { Log.e(TAG, "loadNewItems error", e); subscribeToLoadingChannel(); } @Override public void onNext(List<T> ts) { getAdapter().addNewItems(ts); getAdapter().notifyItemInserted(getAdapter().getItemCount() - ts.size()); if (ts.size() > 0) { subscribeToLoadingChannel(); } } }; loadNewItemsSubscription = getLoadingObservable().getLoadingObservable(offsetAndLimit) .subscribeOn(Schedulers.from(BackgroundExecutor.getSafeBackgroundExecutor())) .observeOn(AndroidSchedulers.mainThread()) .subscribe(loadNewItemsSubscriber); } /** * required method * call in OnDestroy(or in OnDestroyView) method of Activity or Fragment */ public void onDestroy() { scrollLoadingChannel.onCompleted(); if (subscribeToLoadingChannelSubscription != null && !subscribeToLoadingChannelSubscription.isUnsubscribed()) { subscribeToLoadingChannelSubscription.unsubscribe(); } if (loadNewItemsSubscription != null && !loadNewItemsSubscription.isUnsubscribed()) { loadNewItemsSubscription.unsubscribe(); } } } 


According to AutoLoadingRecyclerView it should also be noted that we should not forget about the life cycle and possible memory leaks from RxJava. Therefore, when we “kill” our list, we must remember to unsubscribe from all Subscribers .
And now let's take a look at the specific practical application of our list:
 /** * A placeholder fragment containing a simple view. */ public class MainActivityFragment extends Fragment { private final static int LIMIT = 50; private AutoLoadingRecyclerView<Item> recyclerView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_main, container, false); init(rootView); return rootView; } @Override public void onResume() { super.onResume(); //          //         recyclerView.startLoading(); } private void init(View view) { recyclerView = (AutoLoadingRecyclerView) view.findViewById(R.id.RecyclerView); GridLayoutManager recyclerViewLayoutManager = new GridLayoutManager(getActivity(), 1); recyclerViewLayoutManager.supportsPredictiveItemAnimations(); LoadingRecyclerViewAdapter recyclerViewAdapter = new LoadingRecyclerViewAdapter(); recyclerViewAdapter.setHasStableIds(true); recyclerView.setLayoutManager(recyclerViewLayoutManager); recyclerView.setLimit(LIMIT); recyclerView.setAdapter(recyclerViewAdapter); recyclerView.setLoadingObservable(offsetAndLimit -> EmulateResponseManager.getInstance().getEmulateResponse(offsetAndLimit.getOffset(), offsetAndLimit.getLimit())); } @Override public void onDestroyView() { recyclerView.onDestroy(); super.onDestroyView(); } } 

The difference between the AutoLoadingRecyclerView and the standard RecyclerView only in the addition of the setLimit, setLoadingObservable, onDestroy startLoading . And in the box we have a self-loading list. In my opinion, it is very convenient, capacious and beautiful.

You can look at the source code with a practical example on GitHub . So far, the AutoLoadingRecyclerView is a more practical implementation of the idea, rather than a class, which is easily customized to any developer needs. Therefore, I will be very happy for your comments, suggestions, my AutoLoadingRecyclerView vision and comments.

I would like to express special thanks to lNevermore for preparing this material.

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


All Articles