Android Data Binding for RecyclerView: flexible way



З часу першого анонсу на Google IO 2015 нової бібліотеки Data Binding Library минуло чимало часу. З'явилося багато прикладів, багато гайдів і багато виправлень і доробок у самій бібліотеці. Ось вже і биндинг став two-way, і посилатися на інші View id їх можна в самому layout-файлі та й армія шанувальників цієї бібліотеки неухильно зростає. І, напевно, кожен новий адепт починає з пошуку прикладів — як правильно використовувати так щоб і зручно, і менше коду, та по-феншуй. Якщо зараз вбити запит на подобі «Android DataBinding + RecyclerView» то, напевно, отримаємо цілу купу посилань на різні гайди. Навіть на Хабре вже була подібна стаття — Android Data Binding in RecyclerView.
Але не дивлячись на таку велику кількість ресурсів/гайдів, багато з них показують базовий функціонал, і кожен розробник, починаючи активно використовувати 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. Якщо коротко, то уявімо таку ситуацію: нам дають завдання реалізувати простеньке додаток з 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());
}
}
}


Обробку кліків по елементам нескладно реалізувати, передавши через биндинг обробник кліків, наприклад, як описано тут — Чоловічий і Data Binding: обробка дій, або використавши будь-який інший, зручний для вас спосіб.

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

Бібліотека з прикладом: DataBinding_For_RecyclerView
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.