- PVSM.RU - https://www.pvsm.ru -

Android Data Binding for RecyclerView: flexible way

Android Data Binding for RecyclerView: flexible way - 1

Со времени первого анонса на Google IO 2015 новой библиотеки Data Binding Library [1] прошло немало времени. Появилось много примеров, много гайдов и много исправлений и доделок в самой библиотеке. Вот уже и биндинг стал two-way, и ссылаться на другие View по их id можно в самом layout-файле да и армия поклонников этой библиотеки неуклонно растет. И, наверное, каждый новый адепт начинает с поиска примеров — как правильно использовать так чтобы и удобно, и меньше кода, и по-феншуй. Если сейчас вбить запрос на подобии «Android DataBinding + RecyclerView» то, наверняка, получим целую кучу ссылок на различные гайды. Даже на Хабре уже была подобная статья — Android Data Binding in RecyclerView [2].
Но не смотря на такое обилие ресурсов/гайдов, многие из них показывают базовый функционал, и каждый разработчик, начиная активно использовать Data Binding, придумывает свой, удобный ему способ работы. Далее будет показан один из таких способов.

Этапы:
— реализация/настройка Адаптера (viewTypes, items, обработка кликов по элементам и внутри самих элементов списка);
— настройка RecyclerView (задать LayoutManager, Adapter, ItemDecorator, ItemAnimator, item divider size, ScrollListener, ...).

Конфигурирование RecyclerView

Оставим пока реализацию адаптера и рассмотрим способ задания конфигурации самого RecyclerView. Самый простой здесь способ, просто присвоить id для RecyclerView и уже в коде задать все параметры:

mBinding.recyclerView.setLayoutManager(new LinearLayoutManager(context)); // + другие настройки.

Второй, часто встречаемый, способ — сделать часть самых банальных инициализаций в коде, например, так:

<android.support.v7.widget.RecyclerView
    app:layoutManager="android.support.v7.widget.GridLayoutManager" />

И наконец, используемый автором способ — использовать класс-посредник, который будет сконфигурирован в коде и применен через биндинг. Профит такого подхода в возможности скрыть «дефолтные» настройки внутри класса-посредника (хелпера), но при этом иметь полный контроль над конфигурированием RecyclerView в одном месте.

<android.support.v7.widget.RecyclerView
    app:listConfig="@{viewModel.listConfig}"/>

В коде:

ListConfig listConfig = new ListConfig.Builder(mAdapter)
        //.setLayoutManagerProvider(new GridLayoutManagerProvider(mCount, mSpanLookup)) //LinearLayoutManager if not set
        //.addItemDecoration(new ColorDividerItemDecoration(color, spacing, SPACE_LEFT | SPACE_TOP, false))
        .setDefaultDividerEnabled(true)
        .addOnScrollListener(new OnLoadMoreScrollListener(mCallback))
        .setItemAnimator(getItemAnimator())
        .setHasFixedSize(true)
        .setItemTouchHelper(getItemTouchHelper())
        .build(context);

:

То, что собой представляет ListConfig

public class ListConfig {
    // Adapter, LayoutManager, ItemAnimator, ItemDecorations, ScrollListeners,
    // ItemTouchHelper, hasFixedSize

    private ListConfig(/*params*/) {
        // init fields
    }

    public void applyConfig(final Context context, final RecyclerView recyclerView) {
        //... apply config
    }
    
    public static class Builder {
        
        public Builder(Adapter adapter) {/*set field*/}
        
        public Builder setLayoutManagerProvider(LayoutManagerProvider layoutManagerProvider){/*set field*/}
        public Builder setItemAnimator(ItemAnimator itemAnimator){/*set field*/}
        public Builder addItemDecoration(ItemDecoration itemDecoration){/*set field*/}
        public Builder addOnScrollListener(OnScrollListener onScrollListener){/*set field*/}
        public Builder setHasFixedSize(boolean isFixedSize){/*set field*/}
        public Builder setDefaultDividerEnabled(boolean isEnabled){/*set field*/}
        public Builder setDefaultDividerSize(int size){/*set field*/}
        public Builder setItemTouchHelper(ItemTouchHelper itemTouchHelper){/*set field*/}

        public ListConfig build(Context context) {
            /*set default values*/
            return new ListConfig(/*params*/);
        }
    }
    
    public interface LayoutManagerProvider {
        LayoutManager get(Context context);
    }

}

Реализация гибкого адаптера

Один из самых интересных вопросов — наследование или композиция. Многие повторяют как мантру «Предпочитай композицию наследованию», но все равно продолжают и дальше плодить наследников от наследников от наследников… Кто еще не знаком с потрясающей статьей на эту тему в применении к Адаптерам списков, обязательно просмотрите — JOE'S GREAT ADAPTER HELL ESCAPE [3]. Если кратко, то представим такую ситуацию: нам дают задание реализовать простенькое приложение с 2-мя списками: список пользователей (User) и список локаций (Location). Ничего сложного, правда?) Создаем два адаптера, — UserAdapter и LocationAdapter, — и, по сути, все. Но тут, в следующем «спринте» (мы же по Agile, верно? ) заказчик хочет добавить еще и рекламу в каждый из этих списков (Advertisment).

public class User implements BaseModel {
    public String name;
    public String avatar;
}
public class Location implements BaseModel {
    public String name;
    public String image;
}
public class Advertisement implements BaseModel {
    public String label;
    public String image;
}

Никаких проблем, говорим мы, создаем еще один адаптер AdvertismentAdapter и наследуем от него оба предыдущих: UserAdapter extends AdvertismentAdapter и LocationAdapter extends AdvertismentAdapter. Все хорошо, все рады, но вот в новом «спринте» клиент хочет еще один список, где будут смешаны все 3 сущности сразу. Как быть теперь?
И вот тут и переходим от наследования к композиции. До этого у нас на каждый список был отдельный адаптер со своими типами (viewTypes), теперь заменим эту систему на один адаптер и 3 делегата на каждый тип элемента списка. Адаптер не будет ничего знать о типах элементов, которые отображает, но знает, что у него есть несколько делегатов, спросив по очереди каждый из которых, можно найти конкретный для нужного элемента списка и делегировать ему создание этого элемента.

Адаптер на делегатах

Android Data Binding for RecyclerView: flexible way - 2

В таком случае, нам уже абсолютно все равно сколько списков и с какими типами элементов будут, любой список формируется как конструктор — набором делегатов.

mAdapter = new DelegateAdapter<>(
        new UserDelegate(actionHandler),
        // or new ModelItemDelegate(User.class, R.layout.item_user, BR.user),
        new LocationDelegate(),
        new AdvertismentDelegate(),
        // ...
    );

Пример реализация делегата (UserDelegate)

public class UserDelegate extends ActionAdapterDelegate<BaseModel, ItemUserBinding> {

    public UserDelegate(final ActionClickListener actionHandler) {
        super(actionHandler);
    }

    @Override
    public boolean isForViewType(@NonNull final List<BaseModel> items, final int position) {
        return items.get(position) instanceof User;
    }

    @NonNull
    @Override
    public BindingHolder<ItemUserBinding> onCreateViewHolder(final ViewGroup parent) {
        return BindingHolder.newInstance(R.layout.item_user, LayoutInflater.from(parent.getContext()), parent, false);
    }

    @Override
    public void onBindViewHolder(@NonNull final List<BaseModel> items, final int position, @NonNull final BindingHolder<ItemUserBinding> holder) {
        final User user = (User) items.get(position);
        holder.getBinding().setUser(user);
        holder.getBinding().setActionHandler(getActionHandler());
    }

    @Override
    public long getItemId(final List<BaseModel> items, final int position) {
        return items.get(position).getId();
    }
}

Что касается DataBinding, то вся магия — в особом ViewHolder:

public class BindingHolder<VB extends ViewDataBinding> extends RecyclerView.ViewHolder {
    private VB mBinding;

    public static <VB extends ViewDataBinding> BindingHolder<VB> newInstance(
            @LayoutRes int layoutId, LayoutInflater inflater, ViewGroup parent, boolean attachToParent) {
        final VB vb = DataBindingUtil.inflate(inflater, layoutId, parent, attachToParent);
        return new BindingHolder<>(vb);
    }

    public BindingHolder(VB binding) {
        super(binding.getRoot());
        mBinding = binding;
    }

    public VB getBinding() {
        return mBinding;
    }
}

Если же, даже лень создавать отдельный делегат для каждого нового типа/вида элемента списка, можно воспользоваться особенностью биндинга и использовать единый универсальный делегат для любого типа:

    // new UserDelegate(actionHandler),
    new ModelItemDelegate(User.class, R.layout.item_user);
    // or
    new ModelItemDelegate(R.layout.item_user, BR.model, (item) -> item instance of User);

ModelItemDelegate

public class ModelItemDelegate<T> extends BaseListBindingAdapterDelegate<T, ViewDataBinding> {
    private final int mModelId;
    private final int mItemLayoutResId;
    private final ViewTypeClause mViewTypeClause;

    public ModelItemDelegate(@NonNull Class<? extends T> modelClass, @LayoutRes int itemLayoutResId) {
        this(itemLayoutResId, BR.model, new SimpleViewTypeClause(modelClass));
    }

    public ModelItemDelegate(@LayoutRes int itemLayoutResId, int modelId, ViewTypeClause viewTypeClause) {
        mItemLayoutResId = itemLayoutResId;
        mViewTypeClause = viewTypeClause;
        mModelId = modelId != 0 ? modelId : BR.model;
    }

    @Override
    public boolean isForViewType(@NonNull List<T> items, int position) {
        return mViewTypeClause.isForViewType(items, position);
    }

    @NonNull
    @Override
    public BindingHolder<ViewDataBinding> onCreateViewHolder(ViewGroup parent) {
        return BindingHolder.newInstance(mItemLayoutResId, LayoutInflater.from(parent.getContext()), parent, false);
    }

    @Override
    public void onBindViewHolder(@NonNull List<T> items, int position, @NonNull BindingHolder<ViewDataBinding> holder) {
        ViewDataBinding binding = holder.getBinding();
        binding.setVariable(mModelId, items.get(position));
        binding.executePendingBindings();
    }

    public interface ViewTypeClause {
        boolean isForViewType(List<?> items, int position);
    }

    public static class SimpleViewTypeClause implements ViewTypeClause {

        private final Class<?> mClass;

        public SimpleViewTypeClause(@NonNull Class<?> aClass) {
            mClass = aClass;
        }

        @Override
        public boolean isForViewType(List<?> items, int position) {
            return mClass.isAssignableFrom(items.get(position).getClass());
        }
    }
}

Обработку кликов по элементам несложно реализовать, передав через биндинг обработчик кликов, например, как описано тут — Android и Data Binding: обработка действий [4], или использовав любой другой, удобный для вас, способ.

Заключение

Таким образом, используя Android Data Binding Library, реализация списков становиться совершенно обыденной вещью. Даже не нужно писать реализацию показанных выше вещей, а просто импортировав готовую библиотеку автора, или просто «скопипастив» их оттуда:)

Библиотека с примером: DataBinding_For_RecyclerView [5]

Автор: d_romka

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/183527

Ссылки в тексте:

[1] Data Binding Library: https://developer.android.com/topic/libraries/data-binding/index.html

[2] Android Data Binding in RecyclerView: https://habrahabr.ru/company/dataart/blog/267735/

[3] JOE'S GREAT ADAPTER HELL ESCAPE: http://hannesdorfmann.com/android/adapter-delegates

[4] Android и Data Binding: обработка действий: https://habrahabr.ru/post/305916/

[5] DataBinding_For_RecyclerView: https://github.com/drstranges/DataBinding_For_RecyclerView

[6] Источник: https://habrahabr.ru/post/308872/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best