Pull to refresh

Android Data Binding for RecyclerView: flexible way

Reading time7 min
Views14K


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

Но не смотря на такое обилие ресурсов/гайдов, многие из них показывают базовый функционал, и каждый разработчик, начиная активно использовать Data Binding, придумывает свой, удобный ему способ работы. Далее будет показан один из таких способов.
Пример тут: DataBinding_For_RecyclerView

Этапы:

— реализация/настройка Адаптера (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. Если кратко, то представим такую ситуацию: нам дают задание реализовать простенькое приложение с 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 делегата на каждый тип элемента списка. Адаптер не будет ничего знать о типах элементов, которые отображает, но знает, что у него есть несколько делегатов, спросив по очереди каждый из которых, можно найти конкретный для нужного элемента списка и делегировать ему создание этого элемента.

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


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


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: обработка действий, или использовав любой другой, удобный для вас, способ.

Заключение


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

Библиотека с примером: DataBinding_For_RecyclerView
Tags:
Hubs:
+16
Comments7

Articles