📜 ⬆️ ⬇️

Simplify the work with RecyclerView

On Habré is already full of articles on this topic, all of them basically offer solutions for convenient reuzing cells in RecyclerView . Today we will go a little further and get closer to simplicity comparable to DataBinding .



If you are not using DataBinding for lists (a good example ) and do it the old-fashioned way, then this article is for you.

Introduction


To quickly dive into the context of the article, let's look at what we now have when using the universal adapter from previous articles:
')
  1. Easy work with lists - RendererRecyclerViewAdapter
  2. Easy work with lists - RendererRecyclerViewAdapter (part 2)

To implement the simplest list using the RendererRecyclerViewAdapter v2.xx, you need:

In order for each cell model to implement an empty ViewModel interface:

public class YourModel implements ViewModel { ... public String getYourText() { ... } } 

Make the classic ViewHolder implementation:

 <?xml version="1.0" encoding="utf-8"?> <TextView android:id = "@+id/yourTextView" xmlns:android = "http://schemas.android.com/apk/res/android" android:layout_width = "match_parent" android:layout_height = "50dp" /> 

 public class YourViewHolder extends RecyclerView.ViewHolder { public TextView yourTextView; public RectViewHolder(final View itemView) { super(itemView); yourTextView = (TextView) itemView.findViewById(R.id.yourTextView); } ... } 

Implement ViewRenderer :

 public class YourViewRenderer extends ViewRenderer<YourModel, YourViewHolder> { public YourViewRenderer(Class<YourModel> type, Context context) { super(type, context); } public void bindView(YourModel model, YourViewHolder holder) { holder.yourTextView.setText(model.getYourText()); ... } public YourViewHolder createViewHolder(ViewGroup parent) { return new YourViewHolder(inflate(R.layout.your_layout, parent)); } } 

Initialize the adapter and transfer the necessary data to it:

 ... RendererRecyclerViewAdapter adapter = new RendererRecyclerViewAdapter(); adapter.registerRenderer(new YourViewRenderer(YourModel.class, getContext())); adapter.setItems(getYourModelList()); ... 

Knowing about the DataBinding and its ease of implementation, the question arises - why so much extra code, because the main thing is binding - comparing the model data with a leyout, from which there is no way to go.

In the classical implementation, we use the bindView () method, everything else is just a preparation for it (implementation and initialization of ViewHolder ).

What is a ViewHolder and why is it needed?


We often use this pattern in fragments, activites and recyclers, so why do we need it? What are the pros and cons of him?

Pros:


Minuses:


Third-party libraries such as ButterKnife do an excellent job with some drawbacks, but in the case of RecyclerView this will not help us much - we will not get rid of ViewHolder itself. In DataBinding, we can create a universal viewholder, since this responsibility of the binding lies in the xml itself. What can we do?

We create default ViewHolder


If we use the standard implementation of RecyclerView.ViewHolder as a stub in the createViewHolder () method, then every time in bindView () we will have to use the findViewById method, let's sacrifice the pluses and still see what happens.

Since the class is abstract, we add an empty implementation of the new default viewholder:

 public class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(View itemView) { super(itemView); } } 

Replace the viewholder in our ViewRender:

 public class YourViewRenderer extends ViewRenderer<YourModel, ViewHolder> { public YourViewRenderer(Class<YourModel> type, Context context) { super(type, context); } public void bindView(YourModel model, ViewHolder holder) { ((TextView)holder.itemView.findViewById(R.id.yourTextView)).setText(model.getYourText()); } public ViewHolder createViewHolder(ViewGroup parent) { return new ViewHolder(inflate(R.layout.your_layout, parent)); } } 

The resulting benefits:


Now let's analyze the lost advantages. Since we view the ViewHolder in the RecyclerView framework, we will only access it in the bindView () method, respectively, the first and third points are not very useful to us:


But we can not sacrifice performance, so let's solve something. The implementation of the viewholder allows us to "cache" the views found. So let's add this to the default viewholder:

 public class ViewHolder extends RecyclerView.ViewHolder { private final SparseArray<View> mCachedViews = new SparseArray<>(); public ViewHolder(View itemView) { super(itemView); } public <T extends View> T find(int ID) { return (T) findViewById(ID); } private View findViewById(int ID) { final View cachedView = mCachedViews.get(ID); if (cachedView != null) { return cachedView; } final View view = itemView.findViewById(ID); mCachedViews.put(ID, view); return view; } } 

Thus, after the first call to bindView (), the viewholder will know about all of its views and subsequent calls will use cached values.

Now we’ll take out all the extras in the base ViewRender , add a new parameter to the constructor for transmitting the Leuute ID and see what happens:

 public class YourViewRenderer extends ViewRenderer<YourModel, ViewHolder> { public YourViewRenderer(int layoutID, Class<YourModel> type, Context context) { super(layoutID, type, context); } public void bindView(YourModel model, ViewHolder holder) { ((TextView)holder.find(R.id.yourTextView)).setText(model.getYourText()); } } 

In terms of the amount of code, it looks much better, there is only one constructor, which is always the same. Do we need to create a new ViewRenderer every time for the sake of one method? I think not, we solve this problem through delegation and an additional parameter in the constructor, we look:

 public class ViewBinder<M extends ViewModel> extends ViewRenderer<M, ViewHolder> { private final Binder mBinder; public ViewBinder(int layoutID, Class<M> type, Context context, Binder<M> binder) { super(layoutID, type, context); mBinder = binder; } public void bindView(M model, ViewHolder holder) { mBinder.bindView(model, holder); } public interface Binder <M> { void bindView(M model, ViewHolder holder); } } 

Adding a cell is reduced to:

 ... adapter.registerRenderer(new ViewBinder<>( R.layout.your_layout, YourModel.class, getContext(), (model, holder) -> { ((TextView)holder.find(R.id.yourTextView)).setText(model.getYourText()); } )); ... 

We list the advantages of this solution:


Conclusion


Having sacrificed insignificant advantages, we got a fairly simple solution for adding new cells, this will undoubtedly simplify the work with RecyclerView .

The article provides only a simple example for understanding, the current implementation allows you to:

Work with nested RecyclerView
 adapter.registerRenderer( new CompositeViewBinder<>( R.layout.nested_recycler_view, // ID   RecyclerView    R.id.recycler_view, // ID RecyclerView   DefaultCompositeViewModel.class, //     getContext(), ).registerRenderer(...) //      Nested RecyclerView ); 


Save and restore cell state when scrolling
 //    scrollState  RecyclerView,   Play Market adapter.registerRenderer( new CompositeViewBinder<>( R.layout.nested_recycler_view, R.id.recycler_view, YourCompositeViewModel.class, getContext(), new CompositeViewStateProvider<YourCompositeViewModel, CompositeViewHolder>() { public ViewState createViewState(CompositeViewHolder holder) { return new CompositeViewState(holder); //   } public int createViewStateID(YourCompositeViewModel model) { return model.getID(); // ID       } }).registerRenderer(...) ); ... public static class YourCompositeViewModel extends DefaultCompositeViewModel { private final int mID; public StateViewModel(int ID, List<? extends ViewModel> items) { super(items); mID = ID; } private int getID() { return mID; } } ... public class CompositeViewState <VH extends CompositeViewHolder> implements ViewState<VH> { protected Parcelable mLayoutManagerState; public <VH extends CompositeViewHolder> CompositeViewState(VH holder) { mLayoutManagerState = holder.getRecyclerView().getLayoutManager().onSaveInstanceState(); } public void restore(VH holder) { holder.getRecyclerView().getLayoutManager().onRestoreInstanceState(mLayoutManagerState); } } 


Work with Payload when using DiffUtil
 adapter.setDiffCallback(new YourDiffCallback()); adapter.registerRenderer(new ViewBinder<>( R.layout.item_layout, YourModel.class, getContext(), (model, holder, payloads) -> { if(payloads.isEmpty()) { //    } else { //    Object yourPayload = payloads.get(0); } } } 


Add progress bar when loading data
 adapter.registerRenderer(new LoadMoreViewBinder(R.layout.item_load_more, getContext())); recyclerView.addOnScrollListener(new YourEndlessScrollListener() { public void onLoadMore() { adapter.showLoadMore(); //     } }); 


More detailed examples can be found on the link .

Poll


The design of the binding looks a bit "ugly":

 ... (model, holder) -> { ((TextView)holder.find(R.id.textView)).setText(model.getText()); ((ImageView)holder.find(R.id.imageView)).setImageResource(model.getImage()); } ... 

As a test, I added a couple of methods to the default ViewHolder :

 public class ViewHolder extends RecyclerView.ViewHolder { ... public ViewHolder setText(int ID, CharSequence text) { ((TextView)find(ID)).setText(text); return this; } } 

Result:

 ... (model, holder) -> holder .setText(R.id.textView, model.getText()) .setImageResource(R.id.imageView, ...) .setVisibility(...) ... 

Source: https://habr.com/ru/post/345954/


All Articles