Live Paged Lists, Architecture Components, and Room… or Realm?
Remember the announcement of Architecture Components? LiveData, ViewModel, Lifecycle, Room?
There was one major question left unanswered.
“If I expose my List as a Flowable/LiveData, then it is eagerly evaluated. How will I show lots of data?”
The common answer is pagination, although then how will you receive proper updates when the database has changed?
Realm had an answer: read the data only when it’s accessed. RealmResults<T>
is thread-local, but just a proxy view. The size of the data set is irrelevant if we only request the few that are actually shown.
Room didn’t have an answer… until the Paging Library was also announced.
About the paging library
Before alpha-5, the paging library provided two particular data source types, namely TiledDataSource
and KeyedDataSource
.
Keyed
is for a scenario when the item N
can be evaluated only with help of item N-1
. It’s tricky and Room doesn’t support it either.
Tiled
is the one we all know and use in our lists — it allows us to get the object directly by an index.
Room’s answer to the Pagination problem
Well, Room came out with an answer that changes everything.
First, we create the database, as usual
@Database(entities = {Task.class}, version = 1)
@TypeConverters({RoomTypeConverters.class})
public abstract class DatabaseManager
extends RoomDatabase {
public abstract TaskDao taskDao();
}
And the Task
entity
@Entity(tableName = Task.TABLE_NAME)
public class Task {
// DIFF CALLBACK, HASHCODE, EQUALS... public static final String TABLE_NAME = "TASK";
public static final String COLUMN_ID = "task_id";
public static final String COLUMN_TEXT = "task_text";
public static final String COLUMN_DATE = "task_date";
@PrimaryKey
@ColumnInfo(name = COLUMN_ID)
private int id;
@ColumnInfo(name = COLUMN_TEXT)
private String text;
@ColumnInfo(name = COLUMN_DATE)
private Date date;
...
Then we create a DAO class:
@Dao
public interface TaskDao {
@Query("SELECT * FROM " + Task.TABLE_NAME + " ORDER BY "
+ Task.COLUMN_DATE + " ASC ")
LivePagedListProvider<Integer, Task> tasksSortedByDate();
// Replaced by DataSource.Factory<Integer, Task>
...
And then we are filled with awe.
That’s it? LivePagedListProvider<Integer, Task>
and I get updates from the database when a write occurs? And this is provided as a PagedList<Task>
?
And I can even show this in a RecyclerView??
public class TaskAdapter
extends PagedListAdapter<Task, TaskAdapter.ViewHolder> {
public TaskAdapter() {
super(Task.DIFF_CALLBACK);
} @Override
public void onBindViewHolder(ViewHolder holder, int position) {
Task task = getItem(position);
if(task != null) {
holder.bind(task);
}
}
They’re even scoped and kept alive across configuration change by ViewModel?!
public class TaskViewModel
extends ViewModel {
private final TaskDao taskDao;
private LiveData<PagedList<Task>> liveResults;
public TaskViewModel() {
taskDao = Injector.get().taskDao();
liveResults = taskDao.tasksSortedByDate()
.create(0, new PagedList.Config.Builder()
.setPageSize(20)
.setPrefetchDistance(20)
.setEnablePlaceholders(true)
.build());
} public LiveData<PagedList<Task>> getTasks() {
return liveResults;
}
}
And I can just observe this without any questions asked?!?!
public class TaskFragment
extends Fragment {
RecyclerView recyclerView;
...
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
...
TaskViewModel viewModel = ViewModelProviders.of(this).get(TaskViewModel.class);
...
final TaskAdapter taskAdapter = new TaskAdapter();
recyclerView.setAdapter(taskAdapter); viewModel.getTasks().observe(this, pagedList -> {
taskAdapter.setList(pagedList);
});
}
}
Okay, so that all works super-well together, everything is paginated, and evaluated on a background thread!
In fact, that’s super amazing! It left me wonder: how would I do this with Realm? Not that it’s really needed because Realm already lazy-loads everything… but what if I wanted paginated detached objects queried from a background thread, while keeping reactive updates?
Making Realm work with Paging Library
There’s one important thing to note here:
The pagination library is independent from Room, and allows any data source to be exposed as a TiledDataSource.
So let’s make it work with Realm, shall we?
The end result I had more than half a year ago
First we have our Task
public class Task extends RealmObject {
// DIFFUTIL ITEMCALLBACK (DIFFCALLBACK), HASHCODE, EQUALS
@PrimaryKey
private int id;
private String text;
private Date date;
Then we have our Dao
@Singleton
public class TaskDao {
private final RealmPaginationManager realmPaginationManager;
@Inject
TaskDao(RealmPaginationManager paginationManager) {
this.realmPaginationManager = paginationManager;
}
public LivePagedListProvider<Integer, Task> tasksSortedByDate() {
return realmPaginationManager.createLivePagedListProvider(
realm -> realm.where(Task.class)
.findAllSorted(TaskFields.DATE)); // not Async
}
}
And we have this ominous RealmPaginationManager
which I had to write, which can be open()
ed or close()
d, and is just a regular singleton otherwise.
@Module
public class DatabaseModule {
@Provides
@Singleton
RealmPaginationManager realmPaginationManager() {
return new RealmPaginationManager();
}
}
When the manager is opened, it creates a background HandlerThread, which is the thread the queries will be executed on, and where Realm also listens for notifications.
So assuming open()
is called at some point (although I’d love to make this automatic), we can actually keep the exact same code as above!
Yep, the ViewModel code using Realm looks like this:
public class TaskViewModel
extends ViewModel {
private final TaskDao taskDao;
private LiveData<PagedList<Task>> liveResults;
public TaskViewModel() {
taskDao = Injector.get().taskDao();
liveResults = taskDao.tasksSortedByDate().create(0,
new PagedList.Config.Builder()
.setPageSize(20)
.setPrefetchDistance(20)
.setEnablePlaceholders(true)
.build());
}
public LiveData<PagedList<Task>> getTasks() {
return liveResults;
}
}
The exact same code as before! We just get a RealmLivePagedListProvider<T>
under the hood from the dao. Sweet!
So how the hell does it work?
Most of it is just copy-paste from the Paging Library to allow specifying my own custom Executor (of the handler thread) instead of the io()
scheduler, otherwise the core of it is the RealmTiledDataSource<T>
:
class RealmTiledDataSource<T extends RealmModel> extends TiledDataSource<T> {
private final RealmResults<T> liveResults;
private final RealmChangeListener changeListener =
element -> {
invalidate();
}; ... @Override
@WorkerThread
public List<T> loadRange(int startPosition, int count) {
int countItems = countItems();
List<T> list = new ArrayList<>(count); for(int i = startPosition;
i < startPosition + count && i < countItems; i++) {
list.add(workerRealm.copyFromRealm(liveResults.get(i)));
} return Collections.unmodifiableList(list);
}
}
So the data-source only reads a window of the internal RealmResults at a time, and the objects are detached from the Realm on the background looper thread.
Where can I find this?
All this Realm-Pagination
stuff I was talking about was more a proof of concept than a library at this time, so currently it is not available by just adding a dependency to your Gradle file. But I was experimenting with it here.
What’s worth knowing is that I worked on integrating Realm with Paging once it became stable, and make it as easy to use as possible — for that, you can check out Realm-Monarchy (and read about it here).
DataSource.Factory<Integer, RealmDog> realmDataSourceFactory =
monarchy.createDataSourceFactory(realm ->
realm.where(RealmDog.class));DataSource.Factory<Integer, Dog> dataSourceFactory =
realmDataSourceFactory.map(
input -> Dog.create(input.getName()));LiveData<PagedList<Dog>> dogs = monarchy.findAllPagedWithChanges(
realmDataSourceFactory,
new LivePagedListBuilder<>(dataSourceFactory, 20));dogs.observe(this, dogs -> {...});
Conclusion
Personally, I hope this kind of pagination support will be a first-party citizen to Realm (once Paging Library reaches stable release) .
It is the missing key element to create offline-first applications that can handle larger data sets, while keeping the data layer strictly separated (see clean architecture), off the UI thread (but without using a large amount of memory), and abiding the newly established standards — using Architecture Components.
Live paged lists are awesome! Check it out with Monarchy and its LiveData<PagedList<T>>
support.