How to easily add Nested RecyclerView

Vitaly Vivchar
AndroidPub
Published in
6 min readJan 9, 2018

Last time we optimized the way we worked with RecyclerView, and we also learned how to reuse cells in different lists and easily add new cells.

Today I’ll be explaining two things:

· simplifying DiffUtil support in this implementation;

· adding support for nesting RecyclerView.

If you liked the previous post, then you’ll like this one, too.

DiffUtil

I think there’s no need to explain what DiffUtil is. Every Android developer has probably used it in projects and took advantage of nice extras like animation and improved performance.

A few days after the first article has gone live, I’ve got a pull request with a DiffUtil implementation. Let’s see how it’s implemented. You may remember that we ended up with an adapter with public method setItems(ArrayList <ItemModel> items). Here it’s not very convenient for us to use DiffUtil, and we’ll need to save the old copy of the list somewhere. So, we would be getting something like this:

final MyDiffCallback diffCallback = new MyDiffCallback(getOldItems(), getNewItems());final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);mRecyclerViewAdapter.setItems(getNewItems());diffResult.dispatchUpdatesTo(mRecyclerViewAdapter);

Classic implementation of DiffUtil.Callback:

public class MyDiffCallback extends DiffUtil.Callback 
{
private final List<BaseItemModel> mOldList;
private final List<BaseItemModel> mNewList;
public MyDiffCallback(List<BaseItemModel> oldList,
List<BaseItemModel> newList) {
mOldList = oldList;
mNewList = newList;
}

@Override
public int getOldListSize() {
return mOldList.size();
}

@Override
public int getNewListSize() {
return mNewList.size();
}
@Override
public boolean areItemsTheSame(int old, int new) {
return mOldList.get(old).getID()
== mNewList.get(new).getID();
}
@Override
public boolean areContentsTheSame(int old, int new) {
BaseItemModel oldItem = mOldList.get(old);
BaseItemModel newItem = mNewList.get(new);
return oldItem.equals(newItem);
}
public Object getChangePayload(int old, int new) {
return super.getChangePayload(old, new);
}
}

And an extended ItemModel interface:

public interface BaseItemModel extends ItemModel {
int getID();
}

This is quite implementable and not complicated. However, if we use this several times in a project, then let us think if we really need so much of the same code. Let us carry over the reusable elements into our own DiffUtil.Callback implementation:

public abstract static class DiffCallback 
<BM extends ItemModel>
extends DiffUtil.Callback
{
private final List<BM> mOldItems = new ArrayList<>();
private final List<BM> mNewItems = new ArrayList<>();
void setItems(List<BM> oldItems, List<BM> newItems) {
mOldItems.clear();
mOldItems.addAll(oldItems);
mNewItems.clear();
mNewItems.addAll(newItems);
}
@Override
public int getOldListSize() {
return mOldItems.size();
}
@Override
public int getNewListSize() {
return mNewItems.size();
}
@Override
public boolean areItemsTheSame(int old, int new) {
return areItemsTheSame(
mOldItems.get(old),
mNewItems.get(new)
);
}
public abstract boolean areItemsTheSame(BM oldItem, BM newItem); @Override
public boolean areContentsTheSame(int old, int new) {
return areContentsTheSame(
mOldItems.get(old),
mNewItems.get(new)
);
}
public abstract boolean areContentsTheSame(BM oldItem,
BM newItem);

}

All in all, we managed to make a rather versatile implementation. We avoided unnecessary lines of code and were able to focus on the important methods: areItemsTheSame() and areContentsTheSame(). Those must be implemented and those may differ.

I’m omitting the implementation of getChangePayload() and you can see it in the source.

Now we can add one more method with DiffUtil support in our adapter:

public void setItems(List<ItemModel> items, 
DiffCallback callback) {
callback.setItems(mItems, items);
final DiffResult diffResult = DiffUtil.calculateDiff(callback);
mItems.clear();
mItems.addAll(items);
diffResult.dispatchUpdatesTo(this);
}

That’s about all with DiffUtil, and now we can use our abstract class DiffCallback if necessary. And we’ll be implementing just two methods.

I think now that we’ve warmed up and refreshed our memories, we can move on to more interesting things.

Nesting RecyclerViews

One way or another, nested lists find their ways into our applications, at customer request or at GUI designers discretion,. Until recently I disregarded them as I faced the following problems:

· complex implementation of the cell that contains RecyclerView;

· complex data updating in nesting cells;

· non-reusable nesting cells;

· code reduplication;

· too complex click forwarding from nesting cells into the root location — Fragment/Activity;

Some of these problems are questionable and easily addressable. Some will go away if we plug in our optimized adapter from the first article :) However, we’ll have to face the complexity of implementation at least. Let us articulate our requirements:

· easy addition of new types of nested cells;

· reusability of a cell type for both nested and parent list items;

· simple implementation;

It’s important to note that I have divided the concepts of a cell and a list item:
list item — the entity used in RecyclerView.
cell — a set of classes that allow for showing one list item type. In our case, this is an implementation of already known classes and interfaces: ViewRenderer, ItemModel, ViewHolder.

Let’s sum up what we have at our disposal. ItemModel is the key interface, and it’s obvious it will be convenient for us to work with it later on. Our composite model must include child models, so we add a new interface:

public interface CompositeItemModel extends ItemModel {
List<ItemModel> getItems();
}

Looks decent. Accordingly, composite ViewRenderer must know about child renderers, so we add:

public abstract class CompositeViewRenderer 
<M extends CompositeItemModel, VH extends CompositeViewHolder>
extends ViewRenderer<M, VH>
{
private final ArrayList<ViewRenderer> mRenderers =
new ArrayList<>();
public CompositeViewRenderer(int viewType, Context context) {
super(viewType, context);
}
public CompositeViewRenderer(int viewType,
Context context,
ViewRenderer… renderers) {
super(viewType, context);
Collections.addAll(mRenderers, renderers);
}
public CompositeViewRenderer registerRenderer(ViewRenderer r) {
mRenderers.add(r);
return this;
}
public void bindView(M model, VH holder) {} public VH createViewHolder(ViewGroup parent) {
return …;
}

}

Here I’ve added two ways of adding child renderers, and I’m sure we’ll put them to good use.
Also, please note generic CompositeViewHolder. This will be a separate class for composite ViewHolder, and I don’t know yet what it will contain. Now let us continue working with CompositeViewRenderer. We have two required methods left — bindView() and createViewHolder(). We should initialize the adapter in createViewHolder() and then familiarize it with the renderers. As for bindView(), we’ll do a simple default item updating:

public abstract class CompositeViewRenderer 
<M extends CompositeItemModel, VH extends CompositeViewHolder>
extends ViewRenderer<M, VH>
{
private final ArrayList<ViewRenderer> mRenderers =
new ArrayList<>();
private RendererRecyclerViewAdapter mAdapter;
public void bindView(M model, VH holder) {
mAdapter.setItems(model.getItems());
mAdapter.notifyDataSetChanged();
}
public VH createViewHolder(ViewGroup parent) {
mAdapter = new RendererRecyclerViewAdapter();
for (final ViewRenderer renderer : mRenderers) {
mAdapter.registerRenderer(renderer);
}
return ???;
}

}

Almost there! As it turned out, for such an implementation we’ll need viewHolder itself in createViewHolder(). We cannot initialize it here, so we create a separate abstract method. Here, we may want to familiarize our adapter with RecyclerView. We could borrow it from CompositeViewHolder. We haven’t implemented it yet, so let’s do it:

public abstract class CompositeViewHolder 
extends RecyclerView.ViewHolder
{
public RecyclerView mRecyclerView; public CompositeViewHolder(View itemView) {
super(itemView);
}
}
public abstract class CompositeViewRenderer
<M extends CompositeItemModel, VH extends CompositeViewHolder>
extends ViewRenderer<M, VH>
{
public VH createViewHolder(ViewGroup parent) {
mAdapter = new RendererRecyclerViewAdapter();
for (final ViewRenderer renderer : mRenderers) {
mAdapter.registerRenderer(renderer);
}
VH viewHolder = createCompositeViewHolder(parent);
viewHolder.mRecyclerView.setLayoutManager(
createLayoutManager()
);
viewHolder.mRecyclerView.setAdapter(mAdapter);
return viewHolder;
}
public abstract VH createCompositeViewHolder(ViewGroup parent); protected RecyclerView.LayoutManager createLayoutManager() {
return new LinearLayoutManager(
getContext(), HORIZONTAL, false
);
}

}

That’s right! I’ve added the default implementation with LinearLayoutManager :( I thought this would bring more profit, and we’ll overload the method if necessary and set another LayoutManager.

It seems that’s about it. Now we have specific classes to implement, and we’ll see what we get:

public class SomeCompositeItemModel implements CompositeItemModel 
{
public static final int TYPE = 999;
private int mID;
private final List<ItemModel> mItems;

public SomeCompositeItemModel(int ID, List<ItemModel> items) {
mID = ID;
mItems = items;
}
public int getID() {
return mID;
}

public int getType() {
return TYPE;
}

public List<ItemModel> getItems() {
return mItems;
}
}
public class SomeCompositeViewHolder extends CompositeViewHolder
{
public SomeCompositeViewHolder(View view) {
super(view);
mRecyclerView = view.findViewById(R.id.recycler_view);
}
}
public class SomeCompositeViewRenderer
extends CompositeViewRenderer
<SomeCompositeModel, SomeCompositeViewHolder>
{

public SomeCompositeViewRenderer(int viewType, Context context) {
super(viewType, context);
}
public SomeCompositeViewHolder createCompositeViewHolder(
ViewGroup parent) {
return new SomeCompositeViewHolder(
inflate(R.layout.layout, parent)
);
}
}

We register our composite renderer:

public class SomeActivity extends AppCompatActivity 
{
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SomeCompositeViewRenderer composite = new SomeCompositeViewRenderer(
SomeCompositeItemModel.TYPE,
this,
new SomeViewRenderer(SomeModel.TYPE, this, mListener)
);
mRecyclerViewAdapter.registerRenderer(composite);
}

}

As seen from the last sample, to subscribe to clicks we just pass the necessary interface into the renderer’s constructor. Thus, our root location implements this interface and is aware of all the required clicks.

Click forwarding example:

public class SomeViewRenderer 
extends ViewRenderer<SomeModel, SomeViewHolder>
{
private final Listener mListener; public SomeViewRenderer(int type, Context context, Listener l) {
super(type, context);
mListener = l;
}
public void bindView(SomeModel model, SomeViewHolder holder) {
holder.itemView.setOnClickListener(new OnClickListener() {
public void onClick(final View view) {
mListener.onSomeItemClicked(model);
}
});
}

public interface Listener {
void onSomeItemClicked(SomeModel model);
}
}

Conclusion

We’ve achieved sufficient versatility and flexibility while working with nested lists. We’ve essentially simplified the process of adding composite cells. Now we can easily add new composite cells and easily combine individual cells in nested and parent lists.

Next article

--

--