We simplify work with RecyclerView as much as possible

    Habré is already full of articles on this topic, all of them mainly offer solutions for conveniently reizing cells in RecyclerView . Today we will go a little further and come closer to simplicity comparable to DataBinding .



    If you are not using DataBinding for lists yet (a good example ) and are doing 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 the previous articles:

    1. Easy List Management - RendererRecyclerViewAdapter
    2. Easy List Management - RendererRecyclerViewAdapter (Part 2)

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

    For each cell model to implement an empty ViewModel interface :

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

    Make a classic implementation of ViewHolder :


    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 {
        public  YourViewRenderer(Class 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 pass the necessary data to it:

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

    Knowing about DataBinding and its ease of implementation, the question arises - why is there so much extra code, because the main thing is binding - comparing model data with a layout, which can not be avoided.

    In the classic implementation, we use the bindView () method , everything else is just preparing for it (implementing and initializing the ViewHolder ).

    What is ViewHolder and why is it needed?


    In fragments, activites and recycler views, we often use this pattern, so why do we need it? What are the pros and cons of it?

    Pros:

    • There is no need to use findViewById each time and specify an ID;
    • every time you do not need to spend CPU time searching for a specific ID in xml;
    • it is convenient to access the element anywhere through the created field.

    Minuses:

    • it is necessary to write an additional class;
    • it is necessary for each ID in xml to create a field with a similar name;
    • when changing the ID, you must rename the field in the viewer.

    Third-party libraries, such as ButterKnife , do a great job with some of the minuses , but in the case of RecyclerView this will not help us much - we won’t get rid of ViewHolder itself . In DataBinding, we can create a universal viewer, since this binding is the responsibility of xml itself. What can we do?

    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 be forced 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 viewer:

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

    Replace the viewholder in our ViewRender'e:

    public class YourViewRenderer extends ViewRenderer {
        public  YourViewRenderer(Class 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));
        }
    }
    

    Benefits received:

    • No need to implement ViewHolder for each cell;
    • the implementation of the createViewHolder method can be moved to the base class.

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

    • There is no need to use findViewById each time and specify an ID;
    • every time you do not need to spend CPU time searching for a specific ID in xml;
    • it is convenient to access the element anywhere through the created field.

    But we can’t sacrifice performance, so let's decide something. Implementation of the viewer allows us to “cache” the found views. So let's add this to the default viewholder:

    public class ViewHolder extends RecyclerView.ViewHolder {
        private final SparseArray mCachedViews = new SparseArray<>();
        public ViewHolder(View itemView) {
            super(itemView);
        }
        public  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 viewer will know about all of its views and subsequent calls will use the cached values.

    Now we’ll remove everything unnecessary in the base ViewRender , add a new parameter to the constructor to pass the layout ID and see what happened:

    public class YourViewRenderer extends ViewRenderer {
        public  YourViewRenderer(int layoutID, Class 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 remains one constructor that is always the same. Do we need to create a new ViewRenderer each 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 extends ViewRenderer {
        private final Binder mBinder;
        public  ViewBinder(int layoutID, Class type, Context context, Binder binder) {
            super(layoutID, type, context);
            mBinder = binder;
        }
        public void bindView(M model, ViewHolder holder) {
            mBinder.bindView(model, holder);
        }
        public interface Binder  {
            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:

    • You do not need to create a ViewHolder each time and create variables for the view;
    • You do not need to create a ViewRenderer each time and write extra code;
    • no need to rename anything when changing the view ID;
    • all view data (layoutID, concreteViewID, cast) are in one place.

    Conclusion


    Having sacrificed minor 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() {
                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 items) {
            super(items);
            mID = ID;
        }
        private int getID() {
            return mID;
        }
    }
    ...
    public class CompositeViewState  implements ViewState {
        protected Parcelable mLayoutManagerState;
        public  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();
            // запрос на подгрузку данных
        }
    });
    


    You can find more detailed examples here .

    Poll


    The binding design 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(...)
    ...
    

    Only registered users can participate in the survey. Please come in.

    Do you think it's worth putting the main methods into ViewHolder?

    • 56.6% Yes, it will be very convenient 17
    • 43.3% No, it is better to use a long construction 13

    Also popular now: