Максимально упрощаем работу с RecyclerView
На хабре уже полно статей на эту тему, все они в основном предлагают решения для удобного реюзинга ячеек в RecyclerView. Сегодня мы пойдем немного дальше и приблизимся к простоте сравнимой с DataBinding.
Если вы еще не используете DataBinding для списков (хороший пример) и делаете это по старинке — то эта статья для вас.
Введение
Чтобы быстрее погрузиться в контекст статьи, давайте разберем что мы сейчас имеем при использовании универсального адаптера из предыдущих статей:
- Легкая работа со списками — RendererRecyclerViewAdapter
- Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)
Для реализации самого простого списка с использованием RendererRecyclerViewAdapterv2.x.x вам необходимо:
Чтобы каждая модель ячейки реализовала пустой интерфейс ViewModel:
public class YourModel implements ViewModel {
...
public String getYourText() { ... }
}
Сделать классическую реализацию ViewHolder:
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 {
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));
}
}
Инициализировать адаптер и передать ему необходимые данные:
...
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 {
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));
}
}
Полученные плюсы:
- не нужно реализовывать ViewHolder для каждой ячейки;
- реализацию метода createViewHolder можно вынести в базовый класс.
Теперь проанализируем потерянные плюсы. Так как мы рассматриваем ViewHolder в рамках RecyclerView, то обращаться мы к нему будем только в методе bindView (), соответсвенно перый и третий пункт нам не очень полезены:
- нет необходимости каждый раз использовать findViewById и указывать ID;
- не нужно каждый раз тратить процессорное время на поиск конкретного ID в xml;
- удобно обращаться в любом месте к элементу через созданное поле.
А вот производительностью мы жертвовать не можем, поэтому давайте что-то решать. Реализация вьюхолдера позволяет нам «кэшировать» найденные вью. Так давайте добавим это в дефолтный вьюхолдер:
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;
}
}
Таким образом после первого вызова bindView () вьюхолдер будет знать о всех своих вьюхах и последующие вызовы будут использовать закэшированные значения.
Теперь вынесем все лишнее в базовый ViewRender, добавим новый параметр в конструктор для передачи ID лейяута и посмотрим что получилось:
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());
}
}
С точки зрения количества кода выглядит гораздо лучше, остался один конструктор, который всегда одинаковый. А нужно ли нам каждый раз создавать новый ViewRenderer ради одного метода? Я думаю нет, решаем эту проблему через делегирование и дополнительный параметр в конструкторе, смотрим:
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);
}
}
Добавление ячейки сокращается до:
...
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.
В статье приведен лишь простой пример для понимания, текущая реализация позволяет:
adapter.registerRenderer(
new CompositeViewBinder<>(
R.layout.nested_recycler_view, // ID лейяута с RecyclerView для вложенных ячеек
R.id.recycler_view, // ID RecyclerView в лейяуте
DefaultCompositeViewModel.class, // дефолтная реализация вложенной ячейки
getContext(),
).registerRenderer(...) // добавляем любые типы ячеек внутрь Nested RecyclerView
);
// например для сохранения 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 extends ViewModel> 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);
}
}
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(...)
...