Simplifying RecyclerView Adapters with RxJava & Data Binding

If you‘ve done Android development, you‘ve likely worked with RecyclerViews to display scrollable lists of data. RecyclerView is a powerful and flexible view for showing large data sets in a memory-efficient manner, since it only creates enough views to fill the visible area of the screen and recycles them as the user scrolls.

However, implementing RecyclerView adapters the standard way often involves writing a lot of boilerplate code. You have to create a custom adapter class and ViewHolder, manually bind data to views, and use conditional logic to handle different item view types. It can quickly become verbose and error-prone, especially for more complex adapters.

In this post, we‘ll explore how to leverage two powerful libraries – RxJava and Android data binding – to greatly simplify RecyclerView adapters. By the end, you‘ll be able to create a generic adapter that can bind to any data type and handle multiple view types with minimal code. Let‘s get started!

A Quick RxJava Primer

Before we dive in, let‘s briefly go over some key RxJava concepts. RxJava is a Java implementation of ReactiveX, a library for composing asynchronous event-based programs using observable sequences.

The main building block is an Observable, which emits a stream of data or events over time. Observables can emit any number of items (including zero) and terminate either successfully or with an error.

To receive items emitted by an Observable, you subscribe to it and react to each item in the stream. Observables are cold, meaning they don‘t begin emitting items until something subscribes.

RxJava also provides various operators for transforming, filtering, and combining Observables. For example, you can use map() to transform each item emitted by an Observable, filter() to only emit items matching a certain criteria, or flatMap() to map and flatten emissions from multiple Observables.

Finally, a Subject is a special type of Observable that allows multicasting to multiple subscribers. Unlike regular Observables, Subjects don‘t begin emitting items until an Observer subscribes. PublishSubject in particular emits all items to current and subsequent subscribers.

These are just the basics, but they‘ll be enough for our RecyclerView adapter use case. I highly recommend the RxJava documentation and tutorials to learn more.

Introducing the Generic RxRecyclerAdapter

Now let‘s see how we can apply RxJava to RecyclerView adapters. The goal is to create a generic adapter that can work with any type of data and simplify the binding between the adapter and ViewHolders.

Here‘s a stripped-down version of what our RxRecyclerAdapter class will look like:

public class RxRecyclerAdapter<T, V extends ViewDataBinding> 
       extends RecyclerView.Adapter<RxRecyclerAdapter.ViewHolder<T, V>> {

   private List<T> items = new ArrayList<>();
   private PublishSubject<ViewHolder<T, V>> viewHolderObservable = PublishSubject.create();
   private int layoutRes;

   public RxRecyclerAdapter(int layoutRes) {
       this.layoutRes = layoutRes;
   }

   @Override
   public ViewHolder<T, V> onCreateViewHolder(ViewGroup parent, int viewType) {
       V binding = DataBindingUtil.inflate(
               LayoutInflater.from(parent.getContext()),
               layoutRes,
               parent,
               false);
       return new ViewHolder<>(binding);
   }

   @Override
   public void onBindViewHolder(ViewHolder<T, V> holder, int position) {
       holder.setItem(items.get(position));
       viewHolderObservable.onNext(holder);
   }

   @Override
   public int getItemCount() {
       return items.size();
   }

   public void updateItems(List<T> items) {
       this.items = items;
       notifyDataSetChanged();  
   }

   public Observable<ViewHolder<T, V>> getViewHolderObservable() {
       return viewHolderObservable; 
   }

   public static class ViewHolder<U, W extends ViewDataBinding> extends RecyclerView.ViewHolder {

       private W binding;
       private U item;

       public ViewHolder(W binding) {
           super(binding.getRoot());
           this.binding = binding;
       }

       public void setItem(U item) {
           this.item = item;  
       }

       public U getItem() {
           return item;
       }

       public W getBinding() {
           return binding;
       }
   }
}

Let‘s break this down:

  • The adapter takes two generic type parameters: T for the data type, and V for the ViewDataBinding subclass it will bind to
  • We store the data items in a List and expose a method to update them
  • In onCreateViewHolder, we use data binding to inflate the layout and create the ViewHolder
  • In onBindViewHolder, we set the item on the ViewHolder and emit it to the viewHolderObservable PublishSubject
  • We expose the viewHolderObservable as an Observable so consumers can subscribe to bind to ViewHolders
  • The ViewHolder class is static and also generic, holding references to the data item and the ViewDataBinding

The key piece is the PublishSubject that emits ViewHolders to subscribers. This allows us to bind to Views in a reactive, declarative way from outside the adapter.

Using the RxRecyclerAdapter

So how do we actually use this generic adapter? Let‘s say we have a simple data class:

public class Todo {
    public final String title;
    public final String description;
    public final boolean completed;

    public Todo(String title, String description, boolean completed) {
        this.title = title;
        this.description = description;
        this.completed = completed;
    }
}

And a corresponding item layout in todo_item.xml:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable name="todo" type="com.example.Todo"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="18sp"
            android:text="@{todo.title}"
            tools:text="Todo Item Title"/>

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="14sp"
            android:layout_marginTop="4dp"
            android:text="@{todo.description}"
            tools:text="Todo Item Description"/>

    </LinearLayout>
</layout>

We can then create an instance of our RxRecyclerAdapter and subscribe to it to bind the Todo objects to the views:

//...
RxRecyclerAdapter<Todo, TodoItemBinding> adapter = new RxRecyclerAdapter<>(R.layout.todo_item);
recyclerView.setAdapter(adapter);

List<Todo> todos = getTodos();
adapter.updateItems(todos);

adapter.getViewHolderObservable()
        .subscribe(holder -> {
            TodoItemBinding binding = holder.getBinding();
            Todo todo = holder.getItem();
            binding.setTodo(todo);
        });
//...

And that‘s it! We‘ve bound the Todo data to the views in a concise, reactive way. Whenever the adapter emits a ViewHolder, we simply get the item and set it on the binding.

We can also do powerful things like transform the items using RxJava‘s operators before updating the adapter:

Observable.fromIterable(getTodos())
        .filter(todo -> !todo.completed)
        .map(todo -> new Todo(todo.title, todo.description, false))
        .toList()
        .subscribe(adapter::updateItems);

This filters the list of todos to only those that are not completed, maps them to new Todo objects with completed set to false, collects them into a List, and updates the adapter with the new List.

Supporting Multiple View Types

But what about adapters that need to display different view types? No problem! We can modify our RxRecyclerAdapter to handle this case.

First, let‘s define a ViewHolderInfo class that encapsulates the different view types:

public class ViewHolderInfo {
    public final int layoutRes;
    public final int type;

    public ViewHolderInfo(int layoutRes, int type) {
        this.layoutRes = layoutRes;
        this.type = type;
    }
}

Next, we modify the adapter to take a list of ViewHolderInfos and an ItemTypeCallback to specify the view type for a given position:

public class RxRecyclerAdapter<T, V extends ViewDataBinding> 
       extends RecyclerView.Adapter<RxRecyclerAdapter.ViewHolder<T, V>> {

    //...
    private List<ViewHolderInfo> viewHolderInfos;
    private ItemTypeCallback itemTypeCallback;

    public RxRecyclerAdapter(List<ViewHolderInfo> viewHolderInfos, 
                             ItemTypeCallback itemTypeCallback) {
        this.viewHolderInfos = viewHolderInfos;
        this.itemTypeCallback = itemTypeCallback;
    }

    @Override
    public ViewHolder<T, V> onCreateViewHolder(ViewGroup parent, int viewType) {
        for (ViewHolderInfo info : viewHolderInfos) {
            if (viewType == info.type) {
                V binding = DataBindingUtil.inflate(
                        LayoutInflater.from(parent.getContext()), 
                        info.layoutRes,
                        parent, 
                        false);
                return new ViewHolder<>(binding);
            }
        }
        throw new IllegalArgumentException("View type not found!");
    }

    @Override
    public int getItemViewType(int position) {
        return itemTypeCallback.getItemViewType(position);
    }

    public interface ItemTypeCallback {
        int getItemViewType(int position);
    }

    //...
}

To use it, we provide the list of ViewHolderInfos and implement the ItemTypeCallback:

List<ViewHolderInfo> infos = Arrays.asList(
        new ViewHolderInfo(R.layout.todo_item, 0),
        new ViewHolderInfo(R.layout.todo_header, 1)
);

RxRecyclerAdapter<Todo, ViewDataBinding> adapter = 
        new RxRecyclerAdapter<>(infos, position -> position == 0 ? 1 : 0);

Observable<Todo> todos = Observable.just(
        new Todo("Header", "These are the todos", false),
        new Todo("Todo 1", "Walk the dog", false),
        new Todo("Todo 2", "Buy groceries", false)
);

todos.toList()
        .subscribe(adapter::updateItems);

adapter.getViewHolderObservable()
        .subscribe(holder -> {
            ViewDataBinding binding = holder.getBinding();
            Todo todo = holder.getItem();
            if (binding instanceof TodoItemBinding) {
                ((TodoItemBinding) binding).setTodo(todo);
            } else if (binding instanceof TodoHeaderBinding) {
                ((TodoHeaderBinding) binding).setTodo(todo);
            }
        });

Now we can bind to the different view types and show a header along with the list of todos. The ItemTypeCallback simply returns 1 for position 0 (the header) and 0 for other positions (normal todo items).

Conclusion

To recap, we‘ve seen how RxJava and Android data binding can greatly simplify implementing RecyclerView adapters. By creating a generic, reactive adapter we can bind ViewHolders to data with minimal code while still supporting powerful operations on the data streams.

Some key benefits of this approach:

  • Less boilerplate code for adapters and ViewHolders
  • Declarative, reactive binding between data and views
  • Easily manipulate data using RxJava operators before updating adapter
  • Supports multiple item view types with custom binding logic
  • Generic adapter can be reused across the app for different data types

Potential drawbacks to consider:

  • Steeper learning curve if unfamiliar with RxJava and data binding
  • May be overkill for very simple adapters with static data
  • Data binding has a slight performance/memory overhead
  • Some initial setup required for data binding in app (enabling it in build.gradle, creating binding classes, etc.)

Overall though, for any non-trivial RecyclerView adapter, the benefits of this reactive approach really pay off in terms of more concise, maintainable code. I highly recommend giving it a try in your Android app! The full sample code from this post is available on GitHub.

If you want to dive deeper into RxJava, check out the official documentation and tutorials from ReactiveX. For data binding, the Android developer docs are the best resource to get started.

Thanks for reading! Let me know in the comments if you have any questions or suggestions to improve the RxRecyclerAdapter. Happy coding!

Similar Posts