Binding Realm query results to a RecyclerView

I'm building a calendar app on Android based on event records from an API. It's time to start caching event records on the device to avoid constant network calls.

SQLite or Realm?

A few weeks ago, I learned about Realm.io, a replacement data store for SQLite on Android, and for Core Data on iOS. Realm makes a proud claim, topped by a tempting promise: why bother using the built-in database at all, if an alternative has extremely fast query/write speed and is strongly encrypted? I can't vouch for any of that yet, but I liked its API and thought I'd take it for a spin.

The Realm SDK for Android includes an adapter class that's meant to link the results of Realm queries to an adapter for a ListView. If your models were Java objects retrieved through a SQL data layer, you'd typically pass a typed List of results to your adapter, but the results of a Realm query are in a typed RealmResults object. This SDK class is called RealmBaseAdapter.

My pal, RecyclerView

I'm a fan of the RecyclerView layout in the AppCompat v7 UI library for displaying collections. A standard ListView's management of non-visible list items consumes so much memory that long or complex ListViews will skip and drag. There's a well-known workaround in hardcoding a cache approach: here's Lucas Rocha's canonical post on ViewHolder. But this always struck me as coding cargo-cult style.

So, I'm pleased to see that RecyclerView was designed with this lesson from ListView in mind. RecyclerView expects the developer to contribute a RecyclerView.ViewHolder subclass that it knows exactly what to do with.

Hey, wait: why is ViewHolder so great when used with RecyclerView but not ListView? RecyclerView maintains a pool of recently-visible views for individual items. A RecyclerView.Adapter is purpose-built to require your ViewHolder class to describe the contents of these cached views. Unlike an old-school ArrayAdapter, it doesn't whip up new copies of the same views every time the user scrolls the list.

Now, here's the catch. RealmBaseAdapter is another child class of BaseAdapter, which is also the proud parent of ArrayAdapter. BaseAdapter defines the getView() method, serving up a fresh inner View on the spot and consulting no view cache. Like ArrayAdapter, RealmBaseAdapter doesn't have built-in support for bound and recycled views.

I really wanted to use a RecyclerView, though. I hate typing those words: private static class ViewHolder { ... cringe.

So, I created an implementation of RecyclerView.Adapter that maintains a private RealmBaseAdapter instance. This way, the inner RealmBaseAdapter manages the adapter data and helps its RecyclerView.Adapter wrapper retrieve the right data at the right list position.

RealmRecyclerViewAdapter

My new FrankenAdapter goes by "RealmRecyclerViewAdapter." Catchy, I know. Here are the two key points.

1. Item count is required

RecyclerView.Adapter and BaseAdapter have one concept in common: the count of data items. This count is used to help the layout figure out how many items to display.

In RecyclerView.Adapter, the public int method that returns item count is called getItemCount(). In RealmBaseAdapter, there's a similar method called getCount().

RealmRecyclerViewAdapter is treating its RealmBaseAdapter member variable as its data store, so RealmRecyclerViewAdapter.getItemCount() should return the value of the inner RealmBaseAdapter.getCount(). If the internal RealmBaseAdapter happens to be null, as when Realm result data hasn't been defined yet, then RealmRecyclerViewAdapter.getItemCount() should return 0.

The RecyclerView.Adapter will not attempt to render any views until data has been set and the wrapper's adapter has been notified with notifyDataSetChanged().

2. Stub out BaseAdapter.getView()

You know I'm all about the onCreateViewHolder() and onBindViewHolder() view-caching goodness bestowed by RecyclerView.Adapter. I'm using the inner RealmBaseAdapter instance as a data store and item counter, but not allowing it to construct Views. So, I don't really have a use for the getView() method that RealmBaseAdapter requires to be implemented. Therefore, I just implemented realmBaseAdapter.getView(int position) to return null.

Hack? Feature. Definitely a feature.

A working example

Here is a RealmRecyclerViewAdapter called EventsAdapter destined for my RecyclerView, handling RealmObject models of type Event.

// Event.java
package net.tensory.eventplanner.models;
import java.util.Date;
import io.realm.RealmObject;
import io.realm.annotations.RealmClass;

@RealmClass
public class Event extends RealmObject {
    private String title;
    private Date startTime;

    public Date getStartTime() {
        return startTime;
    }
    public void setStartTime(Date startTime) {
       this.startTime = startTime;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
}

We'll need an implementation of RealmBaseAdapter to provide the inner workings of RealmRecyclerViewAdapter.

RealmBaseAdapter requires you to implement getView(). But as my models and display needs evolve, I don't want to define new RealmBaseAdapters all over the place and stub out RealmBaseAdapter.getView() each time. So, I made a parent class for my future RealmBaseAdapter instances. They won't need getView() defined at creation time.

// RealmModelAdapter.java
package net.tensory.eventplanner.adapters; 
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import io.realm.RealmBaseAdapter;
import io.realm.RealmObject;
import io.realm.RealmResults;

public class RealmModelAdapter<T extends RealmObject> extends RealmBaseAdapter<T> {
    public RealmModelAdapter(Context context, RealmResults<T> realmResults, boolean automaticUpdate) {
        super(context, realmResults, automaticUpdate);
    }

    // I'm not sorry.
    @Override
    public View getView(int position, View convertView, ViewGroup parent) { 
        return null;
    }
}

Here's the RealmRecyclerViewAdapter parent class.

// RealmRecyclerViewAdapter.java
package net.tensory.eventplanner.adapters;
import android.support.v7.widget.RecyclerView;
import io.realm.RealmBaseAdapter;
import io.realm.RealmObject;

/**
 * Wrapper class that allows a RealmBaseAdapter instance to serve
 * as the data source for a RecyclerView.Adapter.
 */
public abstract class RealmRecyclerViewAdapter<T extends RealmObject> extends RecyclerView.Adapter {
    private RealmBaseAdapter<T> realmBaseAdapter;

    /* getItemCount() is not implemented in this class
     * This is left to concrete implementations */

    public void setRealmAdapter(RealmBaseAdapter<T> realmAdapter) {
        realmBaseAdapter = realmAdapter;
    }

    public T getItem(int position) {
        return realmBaseAdapter.getItem(position);
    }

    public RealmBaseAdapter<T> getRealmAdapter() {
        return realmBaseAdapter;
    }
}

And here's the adapter itself, ready to bind to a RecyclerView.

// EventsAdapter.java
package net.tensory.eventplanner.adapters;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import net.tensory.eventplanner.R;
import net.tensory.eventplanner.models.Event;

public class EventsAdapter extends RealmRecyclerViewAdapter<Event> {
    private class EventViewHolder extends RecyclerView.ViewHolder { 
        public TextView tvName;
        public EventViewHolder(View view) {
            super(view);
            tvName = (TextView) view.findViewById(R.id.tv_event_name);
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int i) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_event, parent, false);
        return new EventViewHolder(v);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int i) {
        EventViewHolder evh = (EventViewHolder) viewHolder;
        Event event = getItem(i);
        evh.tvName.setText(event.getTitle());
    }

    /* The inner RealmBaseAdapter
     * view count is applied here.
     * 
     * getRealmAdapter is defined in RealmRecyclerViewAdapter.
     */
    @Override
    public int getItemCount() {
        if (getRealmAdapter() != null) {
            return getRealmAdapter().getCount();
        }
        return 0;
    }
}

Applying the RealmRecyclerViewAdapter

Almost done. Now I do the following steps:

  1. Get a RealmModelAdapter instance with the type T of the models I want to display in my UI list.
  2. Construct a RealmRecyclerViewAdapter.
  3. Feed some data to the RealmModelAdapter.
  4. Link the RealmModelAdapter to the RealmRecyclerViewAdapter.
  5. Wire the RealmRecyclerViewAdapter up to the RecyclerView.

I'm going to need one more convenience class to help create a RealmModelAdapter supporting the Event type I want.

// RealmEventsAdapter.java
package net.tensory.eventplanner.adapters;
import android.content.Context;
import net.tensory.eventplanner.models.Event;
import io.realm.RealmResults;

public class RealmEventsAdapter extends RealmModelAdapter<Event> {
    public RealmEventsAdapter(Context context, RealmResults<Event> realmResults, boolean automaticUpdate) {
        super(context, realmResults, automaticUpdate);
    }
}

I'll skip some of the imports and other setup. Here are the good bits of the Fragment containing my RecyclerView layout:

// EventListViewFragment.java
public class EventListFragment extends Fragment {
    private EventsAdapter adapter;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_event_list, container, false);
        adapter = new EventsAdapter();
        RecyclerView rv = (RecyclerView) view.findViewById(R.id.rv_event_list);
        rv.setLayoutManager(new LinearLayoutManager(getActivity()));
        rv.setAdapter(adapter);
        return view;
    }

    @Override
    public void onResume() {
        // ... calling super.onResume(), etc...
        // Perform the Realm database query
        RealmResults<Event> events = realm.where(Event.class).findAll();
        RealmEventsAdapter realmAdapter = new RealmEventsAdapter(getActivity().getApplicationContext(), events, true);
        // Set the data and tell the RecyclerView to draw
        adapter.setRealmAdapter(realmAdapter);
        adapter.notifyDataSetChanged();
    }
}

Hey, it works!

When I launched my app, I was rewarded with a RecyclerView displaying the few Event items I've recorded so far. Based on a couple hours of playing with Realm, I'm pretty happy with it so far. Soon, of course, I hope for official RecyclerView.Adapter support.