На хабре уже полно статей на эту тему, все они в основном предлагают решения для удобного реюзинга ячеек в RecyclerView. Сегодня мы пойдем немного дальше и приблизимся к простоте сравнимой с DataBinding.

Если вы еще не используете DataBinding для списков (хороший пример) и делаете это по старинке — то эта статья для вас.
Чтобы быстрее погрузиться в контекст статьи, давайте разберем что мы сейчас имеем при использовании универсального адаптера из предыдущих статей:
Для реализации самого простого списка с использованием RendererRecyclerViewAdapter v2.x.x вам необходимо:
Чтобы каждая модель ячейки реализовала пустой интерфейс ViewModel:
Сделать классическую реализацию ViewHolder:
Реализовать ViewRenderer:
Инициализировать адаптер и передать ему необходимые данные:
Зная о DataBinding'e и его простоте реализации, возникает вопрос — зачем столько лишнего кода, ведь основное — это биндинг — сопоставление данных модели с лейяутом, от которого ни куда не уйти.
В классической реализации мы используем метод bindView(), все остальное это лишь подготовка к нему(реализация и инициализация ViewHolder).
Во фрагментах, активити и ресайклер вью мы часто используем этот паттерн, так для чего он нам нужен? Какие плюсы и минусы у него?
Плюсы:
Минусы:
С некоторыми минусами отлично справляются сторонние библиотеки, например ButterKnife, но в случае с RecyclerView нам это не сильно поможет — от самого ViewHolder'a мы не избавимся. В DataBinding мы можем создать универсальный вьюхолдер, так как эта ответственность биндинга лежит в самой xml. Что же можем сделать мы?
Если мы будем использовать стандартную реализацию RecyclerView.ViewHolder как заглушку в методе createViewHolder(), то каждый раз в bindView() мы будем вынуждены использовать метод findViewById, давайте пожертвуем плюсами и все-таки посмотрим что получится.
Так как класс абстрактный — добавим пустую реализацию нового дефолтного вьюхолдера:
Заменим в нашем ViewRender'e вьюхолдер:
Полученные плюсы:
Теперь проанализируем потерянные плюсы. Так как мы рассматриваем ViewHolder в рамках RecyclerView, то обращаться мы к нему будем только в методе bindView(), соответсвенно перый и третий пункт нам не очень полезены:
А вот производительностью мы жертвовать не можем, поэтому давайте что-то решать. Реализация вьюхолдера позволяет нам «кэшировать» найденные вью. Так давайте добавим это в дефолтный вьюхолдер:
Таким образом после первого вызова bindView() вьюхолдер будет знать о всех своих вьюхах и последующие вызовы будут использовать закэшированные значения.
Теперь вынесем все лишнее в базовый ViewRender, добавим новый параметр в конструктор для передачи ID лейяута и посмотрим что получилось:
С точки зрения количества кода выглядит гораздо лучше, остался один конструктор, который всегда одинаковый. А нужно ли нам каждый раз создавать новый ViewRenderer ради одного метода? Я думаю нет, решаем эту проблему через делегирование и дополнительный параметр в конструкторе, смотрим:
Добавление ячейки сокращается до:
Перечислим плюсы такого решения:
Пожертвовав незначительными плюсами мы получили достаточно простое решение для добавления новых ячеек, это несомненно упростит работу с RecyclerView.
В статье приведен лишь простой пример для понимания, текущая реализация позволяет:
Более детальные примеры вы можете найти по ссылке.
Конструкция биндинга выглядит немного «уродливой»:
В качестве пробы я добавил пару методов в дефолтный ViewHolder:
Результат:

Если вы еще не используете DataBinding для списков (хороший пример) и делаете это по старинке — то эта статья для вас.
Введение
Чтобы быстрее погрузиться в контекст статьи, давайте разберем что мы сейчас имеем при использовании универсального адаптера из предыдущих статей:
- Легкая работа со списками — RendererRecyclerViewAdapter
- Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)
Для реализации самого простого списка с использованием RendererRecyclerViewAdapter v2.x.x вам необходимо:
Чтобы каждая модель ячейки реализовала пустой интерфейс ViewModel:
public class YourModel implements ViewModel { ... public String getYourText() { ... } }
Сделать классическую реализацию ViewHolder:
<?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); } ... }
Реализовать 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)); } }
Инициализировать адаптер и передать ему необходимые данные:
... RendererRecyclerViewAdapter adapter = new RendererRecyclerViewAdapter(); adapter.registerRenderer(new YourViewRenderer(YourModel.class, getContext())); adapter.setItems(getYourModelList()); ...
Зная о DataBinding'e и его простоте реализации, возникает вопрос — зачем столько лишнего кода, ведь основное — это биндинг — сопоставление данных модели с лейяутом, от которого ни куда не уйти.
В классической реализации мы используем метод bindView(), все остальное это лишь подготовка к нему(реализация и инициализация ViewHolder).
Что такое ViewHolder и зачем он нужен?
Во фрагментах, активити и ресайклер вью мы часто используем этот паттерн, так для чего он нам нужен? Какие плюсы и минусы у него?
Плюсы:
- нет необходимости каждый раз использовать findViewById и указывать ID;
- не нужно каждый раз тратить процессорное время на поиск конкретного ID в xml;
- удобно обращаться в любом месте к элементу через созданное поле.
Минусы:
- необходимо писать дополнительный класс;
- необходимо для каждого ID в xml создавать поле с подобным названием;
- при изменении ID необходимо переименовывать и поле во вьюхолдере.
С некоторыми минусами отлично справляются сторонние библиотеки, например ButterKnife, но в случае с RecyclerView нам это не сильно поможет — от самого ViewHolder'a мы не избавимся. В DataBinding мы можем создать универсальный вьюхолдер, так как эта ответственность биндинга лежит в самой xml. Что же можем сделать мы?
Создаем дефолтный ViewHolder
Если мы будем использовать стандартную реализацию RecyclerView.ViewHolder как заглушку в методе createViewHolder(), то каждый раз в bindView() мы будем вынуждены использовать метод findViewById, давайте пожертвуем плюсами и все-таки посмотрим что получится.
Так как класс абстрактный — добавим пустую реализацию нового дефолтного вьюхолдера:
public class ViewHolder extends RecyclerView.ViewHolder { public ViewHolder(View itemView) { super(itemView); } }
Заменим в нашем ViewRender'e вьюхолдер:
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)); } }
Полученные плюсы:
- не нужно реализовывать ViewHolder для каждой ячейки;
- реализацию метода createViewHolder можно вынести в базовый класс.
Теперь проанализируем потерянные плюсы. Так как мы рассматриваем ViewHolder в рамках RecyclerView, то обращаться мы к нему будем только в методе bindView(), соответсвенно перый и третий пункт нам не очень полезены:
нет необходимости каждый раз использовать findViewById и указывать ID;- не нужно каждый раз тратить процессорное время на поиск конкретного ID в xml;
удобно обращаться в любом месте к элементу через созданное поле.
А вот производительностью мы жертвовать не можем, поэтому давайте что-то решать. Реализация вьюхолдера позволяет нам «кэшировать» найденные вью. Так давайте добавим это в дефолтный вьюхолдер:
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; } }
Таким образом после первого вызова bindView() вьюхолдер будет знать о всех своих вьюхах и последующие вызовы будут использовать закэшированные значения.
Теперь вынесем все лишнее в базовый ViewRender, добавим новый параметр в конструктор для передачи ID лейяута и посмотрим что получилось:
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()); } }
С точки зрения количества кода выглядит гораздо лучше, остался один конструктор, который всегда одинаковый. А нужно ли нам каждый раз создавать новый ViewRenderer ради одного метода? Я думаю нет, решаем эту проблему через делегирование и дополнительный параметр в конструкторе, смотрим:
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); } }
Добавление ячейки сокращается до:
... adapter.registerRenderer(new ViewBinder<>( R.layout.your_layout, YourModel.class, getContext(), (model, holder) -> { ((TextView)holder.find(R.id.yourTextView)).setText(model.getYourText()); } )); ...
Перечислим плюсы такого решения:
- не нужно каждый раз создавать ViewHolder и создавать переменные для вьюх;
- не нужно каждый раз создавать ViewRenderer и писать лишний код;
- не нужно ничего переименовывать при изменении ID вьюхи;
- все данные о вью(layoutID, concreteViewID, cast) находятся в одном месте.
Заключение
Пожертвовав незначительными плюсами мы получили достаточно простое решение для добавления новых ячеек, это несомненно упростит работу с RecyclerView.
В статье приведен лишь простой пример для понимания, текущая реализация позволяет:
Работать со вложенными RecyclerView
adapter.registerRenderer( new CompositeViewBinder<>( R.layout.nested_recycler_view, // ID лейяута с RecyclerView для вложенных ячеек R.id.recycler_view, // ID RecyclerView в лейяуте DefaultCompositeViewModel.class, // дефолтная реализация вложенной ячейки getContext(), ).registerRenderer(...) // добавляем любые типы ячеек внутрь Nested RecyclerView );
Сохранять и восстанавливать состояние ячейки при cкролле
// например для сохранения 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); } }
Работать с Payload при использовании 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); } } }
Добавлять прогресс бар при подгрузке данных
adapter.registerRenderer(new LoadMoreViewBinder(R.layout.item_load_more, getContext())); recyclerView.addOnScrollListener(new YourEndlessScrollListener() { public void onLoadMore() { adapter.showLoadMore(); // запрос на подгрузку данных } });
Более детальные примеры вы можете найти по ссылке.
Опрос
Конструкция биндинга выглядит немного «уродливой»:
... (model, holder) -> { ((TextView)holder.find(R.id.textView)).setText(model.getText()); ((ImageView)holder.find(R.id.imageView)).setImageResource(model.getImage()); } ...
В качестве пробы я добавил пару методов в дефолтный ViewHolder:
public class ViewHolder extends RecyclerView.ViewHolder { ... public ViewHolder setText(int ID, CharSequence text) { ((TextView)find(ID)).setText(text); return this; } }
Результат:
... (model, holder) -> holder .setText(R.id.textView, model.getText()) .setImageResource(R.id.imageView, ...) .setVisibility(...) ...
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как Вы думаете, стоит ли помещать во ViewHolder основные методы?
58.82%Да, это будет очень удобно20
41.18%Нет, лучше использовать длинную конструкцию14
Проголосовали 34 пользователя. Воздержались 25 пользователей.
