Main question:
What is the correct approach to use an Android RecyclerView
and RecyclerView.Adapter
without holding the whole dataset in memory?
Background
Before I explain my initial question in more detail, let me first elaborate on the background which will help to understand my question more easily.
It seems that there are basically two approaches how to deal with RecyclerView.Adapter
and LiveData
from a ViewModel
:
The "hackish" or "quick-and-dirty" way
It it is not possible to use a
RecyclerView.Adapter
or even a singleViewHolder
as aLiveData
observer out-of-the-box, because neitherRecyclerView.Adapter
norViewHolder
are aLifecycleOwner
. The internet is full of workarounds, how aViewHolder
can observe its own data either by making aViewHolder
aLifecycleOwner
like in this example or by observing theLifeData
forever like in this question. "Observing forever" does not require aLifecycleOwner
, but of course this raises the problem, how aViewHolder
stops observing again.The "official" way
According to this answer one should not use
LiveData
with aRecyclerView.Adapter
directly. Instead, the activity/fragment which holds theRecyclerView
should observe aLifeData
of the whole dataset and notify theRecyclerView.Adapter
, if the dataset has changed. In order to keep the update small for theRecyclerView
a 3-way diff between the old and the new dataset can be implemented. Here's an example of observingLiveData
of a whole dataset in the fragment and using DiffUtil within a RecyclerView adapter.
Detailed question
The official way has two obvious drawbacks and the second one actually makes it impossible for me to use it.
Typically, the underlying repository is able to directly return the total number of entries and to return the i'th object. There is no need to fetch the whole dataset first and then use a copy of the the dataset just to pick out the currently visible objects. Moreover, the repository typically "knows" which item has been inserted, removed or updated. Hence, it feels wrong to let the
Fragment
observe the complete dataset, let theViewModel
post a new version of the complete dataset to the fragment (byMutableLiveData.setValue( List<Entity> )
) and then let the fragment run a 3-way diff between the old and the new dataset in order to find out, what has changed, if the repository already knows these information.The great benefit of a RecyclerView is that is does not create unnecessary
ViewHolder
objects for each entry, but only as manyViewHolder
objects as are visible at the same time and then "recycles" those objects to show new data if the user scrolls through the list. However, this is not true for the underlying dataset, if the the official approach is used. The fragment observes the whole dataset and thus each entity of the dataset must be kept in memory. This is prohibitive, if the the dataset is large or if each entity is a complex object and not only a bunch of POD-types. In that case, it would be really beneficial if the "recycle approach" would stretch out over the dataset, too.
In my particular use case, each entity holds a Drawable
. A ViewHolder
basically contains a ImageView
which shall display that drawable, when the ViewHolder
comes into view. In other words, my onBindViewHolder(ViewHolder holder, int position)
basically would look like that, if I were following the official approach
holder.getImageView().setImageDrawable( mDataset.get( position ).getDrawable() )
Here mDataset
is a deep copy of the whole dataset.
Moreover, the content of each drawable depends on the ID of the entity and is created programmatically.
Hence, there is no real reason to keep all these drawables in an in-memory dataset.
The following approach would be more efficient:
- On binding, the
ViewHolder
obtains aLifeData
of theDrawable
from theViewModel
. - The
ViewHolder
places theDrawable
in theImageView
- The
ViewHolder
starts observing theDrawable
. - Under the hood, when the
ViewModel
provides aLifeData
of theDrawable
to theViewHolder
, theViewModel
triggers the repository to render the real drawable. - The repository renders the final
Drawable
in a background thread. - When the thread has rendered the final
Drawable
, theViewModel
updates theLiveData
- The
ViewHolder
receives a notification and displays the final content in itsImageView
.
In other words, a fake code would look like that
class ViewHolder ... {
...
public void onBindViewHolder( MyViewHolder holder, int position ) {
// N.b. requesting i'th entity returns a placeholder entity
// Such an entity has a drawble of correct size, but the drawable
// shows an illustration which indicates that is has not been finally rendered
// At the same time, `viewModel.getEntity(i)` starts the rendering process
// in the background
LiveData<Entity> liveEntity = viewModel.getEntity( position );
holder.getImageView().setImageDrawable( liveEntity.getValue.getDrawable() );
liveEntity.observe( holder, holder );
}
}
class MyViewHolder ... implements Observer<Entity> {
...
public void onChanged( Entity entity ) {
// Method is called, if background thread has rendered the final drawable
getImageView().setImageDrawable( entity.getDrawable )
}
}
Can this be achieved?