Pull to refresh

Максимально упрощаем работу с RecyclerView

Development of mobile applications *Development for Android *
На хабре уже полно статей на эту тему, все они в основном предлагают решения для удобного реюзинга ячеек в 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(...)
...
Only registered users can participate in poll. Log in, please.
Как Вы думаете, стоит ли помещать во ViewHolder основные методы?
57.58% Да, это будет очень удобно 19
42.42% Нет, лучше использовать длинную конструкцию 14
33 users voted. 25 users abstained.
Tags:
Hubs:
Total votes 13: ↑11 and ↓2 +9
Views 15K
Comments Comments 5