Simplifying RecyclerView Adapters with Rx & Databinding
By Ahmed Rizwan
I recently wanted to dive deeper into Rx. So I experimented with Rx and the RecyclerView Adapters, and the results were pretty interesting!
With Rx in mind, I set out to accomplish three things:
- Create a RecyclerView adapter which should be generic — one adapter class to rule them all!
- It should return bindings in the form of Rx streams!
- There should also be an option for supporting multiple item types!
Now, you may be thinking: this isn’t really necessary. I mean why use Rx in the first place with RecyclerAdapters? And why exactly do you need bindings as Rx streams?
Well that’s true. Personally, I thought it’d be a good experiment to incorporate Rx into RecyclerView Adapters, instead of using simple callbacks or delegates. So it was sort of experimental.
So I wrote a library called RxRecyclerAdapter to get Rx to work with the Adapters. Let’s break down how it simplifies use of the recycler adapters.
RxDataSource simplifies the use of RxRecyclerAdapter
Let’s say you have a beautiful String array list that you want to display:
//Dummy DataSetdataSet = new ArrayList<>();dataSet.add("this");dataSet.add("is");dataSet.add("an");dataSet.add("example");dataSet.add("of rx!");
Here’s what you would do:
- Enable data binding by adding this into build.gradle
dataBinding { enabled = true}
- create the layout file for the item:
<?xml version="1.0" encoding="utf-8"?><layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="@dimen/activity_horizontal_margin"> <;TextView android:id="@+id/textViewItem" android:layout_width="match_parent" android:layout_height="wrap_content" tools:text="Recycler Item"/> </LinearLayout></layout>
- Create an instance of RxDataSource telling it what the dataSet type is:
RxDataSource<String> rxDataSource = new RxDataSource<>(dataSet);
- Compose and then cast-call bindRecyclerView (passing in the RecyclerView and layout) with LayoutBinding. Because of casting, viewHolder can infer the type of binding.
rxDataSource .map(String::toLowerCase) .repeat(10) .<ItemLayoutBinding>bindRecyclerView(recyclerView, R.layout.item_layout) .subscribe(viewHolder -> { ItemLayoutBinding b = viewHolder.getViewDataBinding(); b.textViewItem.setText(viewHolder.getItem()); });
The output will be…
Note that calling observeOn(AndroidSchedulers.mainThread()) would be unnecessary here, as you’re already on the mainThread. And when you call it, it causes a delay of about ~20–30 milliseconds in the stream, which would lower your frame rate.
Now for a bit more practical example.
Let’s say you want to dynamically update the dataSet. Let’s say you want to search the dataSet and filter out the results specific results. Here’s how that would be done:
RxTextView.afterTextChangeEvents(searchEditText).subscribe(event -> { rxDataSource.updateDataSet(dataSet) .filter(s -> s.contains(event.view().getText())) .updateAdapter();});
In combination with RxBindings (because RxBindings are awesome), I register for textChange events. And when the event occurs I update the DataSet with the base dataSet!
Now this is important because the RxDataSource changes its dataSet instance when I call methods like filter, map and so on. So filtering needs to be done on the original dataSet, not the changed one. And… bam!
I did come across some limitations — one being that you can’t change the type of dataSet after it has been bound with the data source. So functions like map and flatmap can’t return a different type of dataSet. But I have yet to run into a situation where I needed to be able to change the dataSet at runtime.
RxRecyclerAdapter simplifies the situations where you have multiple item types
Now let’s say you wanted multiple Item types in your RecyclerView, for example a header and an item type. Then you would:
- Create List of ViewHolderInfo specifying all the layouts
List<ViewHolderInfo> vi = new ArrayList<>();vi.add(new ViewHolderInfo(R.layout.item_layout, TYPE_ITEM)); vi.add(new ViewHolderInfo(R.layout.item_header_layout, TYPE_HEADER));
- Create instance of RxDataSource like before:
RxDataSource<String> rxDataSource = new RxDataSource<>(dataSet);
- Compose and call bindRecyclerView passing in the recyclerView, the viewHolderInfo list and implementation of getItemViewType:
rxDataSource.bindRecyclerView(recyclerView, viewHolderInfoList, new OnGetItemViewType() { @Override public int getItemViewType(int position) { if (position % 2 == 0) { return TYPE_HEADER; //headers are even positions } return TYPE_ITEM; } } ).subscribe(vH -> { //Check instance type and bind! final ViewDataBinding b = vH.getViewDataBinding(); if (b instanceof ItemLayoutBinding) { final ItemLayoutBinding iB = (ItemLayoutBinding) b; iB.textViewItem.setText("ITEM: " + vH.getItem()); } else if (b instanceof ItemHeaderLayoutBinding) { ItemHeaderLayoutBinding hB = (ItemHeaderLayoutBinding) b; hB.textViewHeader.setText("HEADER: " + vH.getItem()); } });
/* and like before, you can do this as well rxDataSource.filter(s -> s.length() > 0) .map(String::toUpperCase) .updateAdapter();*/
Now recyclerView would look something like:
A little about the Implementation
PublishSubject
Preface → I utilized PublishSubjects for the most part, and generics to create the adapter.
PublishSubject is a type of observable which can be both Observable and an Observer at the same time.
Because it is an observer, it can subscribe to one or more Observables. And because it is an Observable, it can pass through the items it observes by reemitting them, and it can also emit new items.
Internals
Internally, there are two adapters, which you can also access directly if you want: RxAdapter and RxAdapterForTypes.
For these two, I created a generic ViewHolder implementation, which binds the layout with an instance of ViewDataBinding:
public class SimpleViewHolder<T, V extends ViewDataBinding> extends RecyclerView.ViewHolder { private V mViewDataBinding; public V getViewDataBinding() { return mViewDataBinding; } public T getItem() { return mItem; } private T mItem; protected void setItem(final T item) { mItem = item; } public SimpleViewHolder(final View itemView) { super(itemView); mViewDataBinding = DataBindingUtil.bind(itemView); }}
Then I created RxAdapter — It takes two generics:
RxAdapter<DataType, LayoutBinding extends ViewDataBinding>
I created a PublishSubject for my ViewHolder, and in onBindViewHolder I call onNext. The viewHolder contains the item itself:
@Overridepublic void onBindViewHolder(final SimpleViewHolder<T, V> holder, final int position) { holder.setItem(mDataSet.get(position)); mPublishSubject.onNext(holder);}
Finally, I created a method asObservable, which returns the publishSubject as an Observable so that you can subscribe to it:
public Observable<SimpleViewHolder> asObservable(){ return mPublishSubject.asObservable();}
But wait, what about the RxDataSource? Well it’s just a wrapper for Rx Observables. It’s main purpose is to provide you with an abstraction over the two adapters and Rx methods. It basically connects everything together.
When I say it’s a wrapper, that means that you only get methods that are relevant to a recyclerAdapter, like filter, map, take, first, repeat and so on. It doesn’t give you methods which have something to do with threading or schedulers.
As the class is pretty straight-forward. You can check out the code for RxDataSource here.
That’s pretty much it… I hope you found this article useful. Do give RxAdapter a try. And if you have any questions (or suggestions), fire away!
Happy coding!
Subscribe to my newsletter
Read articles from freeCodeCamp directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
freeCodeCamp
freeCodeCamp
Learn to code. Build projects. Earn certifications—All for free.