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



Если вы еще не используете DataBinding для списков (хороший пример) и делаете это по старинке — то эта статья для вас.

Введение


Чтобы быстрее погрузиться в контекст статьи, давайте разберем что мы сейчас имеем при использовании универсального адаптера из предыдущих статей:

  1. Легкая работа со списками — RendererRecyclerViewAdapter
  2. Легкая работа со списками — 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(...)
...

Комментарии (5)


  1. zagayevskiy
    15.01.2018 10:42

    Как Вы думаете, стоит ли помещать во ViewHolder основные методы?
    Да, это будет очень удобно
    Нет, лучше использовать длинную конструкцию

    Нет, я думаю, не нужно изобретать велосипеды. Избавились от вьюх-полей -> получили уродливые конструкции -> напишем столько же кода для удобства. Зашибись, чо.


    1. vivchar Автор
      15.01.2018 11:04

      написать код для удобства — это разве проблема?) тем более это будет скрыто за библиотекой, не конечному же пользователю это писать.
      Если вы переживаете за «уродство», то это только лишь пример для статьи, для лучшего понимания


  1. Dimezis
    15.01.2018 17:49

    Как Вы думаете, стоит ли помещать во ViewHolder основные методы?

    Я думаю вообще не стоило делать такого рода ViewHolder. Получается, сломали стандартный ViewHolder, накостылили поверх кэш и теперь в каждом холдере есть лишний SparseArray, а в итоге эта конструкция для байндинга вида
    ((TextView)holder.find(R.id.textView)).setText(model.getText());
    выглядит ничем не лучше, чем ViewHolder с полями.

    При этом даже написав 1000 методов для удобства, все равно не покроете все возможные случаи байндинга ячейки.
    Есть, кстати, либа, которая работает примерно по такому принципу: github.com/MEiDIK/SlimAdapter


    1. expromt
      16.01.2018 05:14

      Конечно, есть специфичные списки и для них действительно лучше воспользоваться обычным ViewHolder. Но, наверное, в 80-90% случаев нужен очень простой список с минимальным функционалом по инициализации элементов. И подход, продемонстрированный в данной статье поможет сильно сократить код.

      Самому постоянно приходится писать большое количество разных списков, и сам постоянно ищу способы максимально оптимизировать этот процесс. Так что спасибо автору за демонстрацию еще одного интересного подхода и полезную библиотеку!

      Как Вы думаете, стоит ли помещать во ViewHolder основные методы?


      Я считаю что стоит — поможет сильно сократить код и добавит удобства.


    1. vivchar Автор
      16.01.2018 05:18

      При этом даже написав 1000 методов для удобства, все равно не покроете все возможные случаи байндинга ячейки.

      как раз для оставшихся случаев можно использовать, конструкцию приведенную вами выше.

      Получается, сломали стандартный ViewHolder

      не сломал, при желании можно использовать ViewRenderer, из предыдущей статьи. В нем можно использовать стандартный ViewHolder.

      Есть, кстати, либа, которая работает примерно по такому принципу

      Да, спасибо, только там возможности RecyclerView урезаны, здесь же все доступно — DiffUtil, Payload и т д