В прошлый раз мы оптимизировали работу с RecyclerView, а так же научились переиспользовать ячейки в разных списках и легко добавлять новые.
Сегодня мы разберем:
- как можно упростить поддержку DiffUtil в этой реализации;
- как добавить поддержку вложенных RecyclerView.
Если прошлая статья тебе пришлась по душе, думаю, понравится и эта.
DiffUtil
Что такое DiffUtil, я думаю разбирать не стоит. Наверное, уже каждый разработчик опробовал его в своем проекте и получил приятные плюшки в виде анимации и производительности.
В первые дни после публикации первой статьи я получил пулл реквест с реализацией DiffUtil, давайте посмотрим как это реализовано. Напомню, что в результате оптимизации у нас получился адаптер с публичным методом setItems(ArrayList <ItemModel> items). В данном виде не очень удобно использовать DiffUtil, нам необходимо где-то дополнительно сохранять старую копию списка, в результате мы получим что-то вроде этого:
...
final MyDiffCallback diffCallback = new MyDiffCallback(getOldItems(), getNewItems());
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
mRecyclerViewAdapter.setItems(getNewItems());
diffResult.dispatchUpdatesTo(mRecyclerViewAdapter);
...
public class MyDiffCallback extends DiffUtil.Callback {
private final List<BaseItemModel> mOldList;
private final List<BaseItemModel> mNewList;
public MyDiffCallback(List<BaseItemModel> oldList, List<BaseItemModel> newList) {
mOldList = oldList;
mNewList = newList;
}
@Override
public int getOldListSize() {
return mOldList.size();
}
@Override
public int getNewListSize() {
return mNewList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).getID() == mNewList.get(
newItemPosition).getID();
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
BaseItemModel oldItem = mOldList.get(oldItemPosition);
BaseItemModel newItem = mNewList.get(newItemPosition);
return oldItem.equals(newItem);
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return super.getChangePayload(oldItemPosition, newItemPosition);
}
}
И расширенный интерфейс ItemModel:
public interface BaseItemModel extends ItemModel {
int getID();
}
В общем-то реализуемо и не сложно, но если это делать в нескольких местах, то стоит задуматься зачем столько много одинакового кода. Попробуем вынести общие моменты в свою реализацию DiffUtil.Callback:
public abstract static class DiffCallback <BM extends ItemModel> extends DiffUtil.Callback {
private final List<BM> mOldItems = new ArrayList<>();
private final List<BM> mNewItems = new ArrayList<>();
void setItems(List<BM> oldItems, List<BM> newItems) {
mOldItems.clear();
mOldItems.addAll(oldItems);
mNewItems.clear();
mNewItems.addAll(newItems);
}
@Override
public int getOldListSize() {
return mOldItems.size();
}
@Override
public int getNewListSize() {
return mNewItems.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return areItemsTheSame(
mOldItems.get(oldItemPosition),
mNewItems.get(newItemPosition)
);
}
public abstract boolean areItemsTheSame(BM oldItem, BM newItem);
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return areContentsTheSame(
mOldItems.get(oldItemPosition),
mNewItems.get(newItemPosition)
);
}
public abstract boolean areContentsTheSame(BM oldItem, BM newItem);
...
}
В общем получилось достаточно универсально, мы избавились от рутинны и сосредоточились на главных методах — areItemsTheSame() и areContentsTheSame(), которые обязательны к реализации и могут отличаться.
Реализация метода getChangePayload() намеренно пропущена, её реализацию можно посмотреть в исходниках.
Теперь мы можем добавить еще один метод с поддержкой DiffUtil в наш адаптер:
public void setItems(List<ItemModel> items, DiffCallback diffCallback) {
diffCallback.setItems(mItems, items);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
mItems.clear();
mItems.addAll(items);
diffResult.dispatchUpdatesTo(this);
}
В общем то с DiffUtil это все, теперь при необходимости мы используем наш абстрактный класс — DiffCallback, и реализуем всего два метода.
Я думаю теперь мы разогрелись и освежили память, значит, можно перейти к более интересным вещам.
Вложенные RecyclerView
Часто по воле заказчика или веянию дизайнеров в нашем приложении появляются вложенные списки. До недавних пор я недолюбливал их, я сталкивался с такими проблемами:
- сложность реализации ячейки, которая содержит RecyclerView;
- сложность обновление данных во вложенных ячейках;
- непереиспользуемость вложенных ячеек;
- дублирование кода;
- запутанность проброса кликов от вложенных ячеек в корневое место — Fragment/Activity;
Некоторые из этих проблем сомнительны и легко решаемы, а некоторые уйдут, если подключить наш оптимизированный адаптер из первой статьи :). Но, как минимум, сложность реализации у нас останется. Давайте сформулируем наши требования:
- возможность легко добавлять новые типы вложенных ячеек;
- переиспользуемость типа ячейки как для вложенного так и для основного элемента списка;
- простота реализации;
Важно заметить, что здесь я разделил понятие ячейка и элемент списка:
элемент списка — сущность используемая в RecyclerView.
ячейка — набор классов, позволяющих отобразить один тип элемента списка, в нашем случае это реализация ранее известных классов и интерфейсов: ViewRenderer, ItemModel, ViewHolder.
И так, что мы имеем. Ключевым интерфесом у нас является ItemModel, очевидно что нам удобно будет далее с ним и работать. Наша композитная модель должна содержать в себе дочерние модели, добавляем новый интерфейс:
public interface CompositeItemModel extends ItemModel {
List<ItemModel> getItems();
}
Выглядит неплохо, соответсвенно, композитный ViewRenderer должен знать о дочерних рендерерах — добавляем:
public abstract class CompositeViewRenderer <M extends CompositeItemModel, VH extends CompositeViewHolder> extends ViewRenderer<M, VH> {
private final ArrayList<ViewRenderer> mRenderers = new ArrayList<>();
public CompositeViewRenderer(int viewType, Context context) {
super(viewType, context);
}
public CompositeViewRenderer(int viewType, Context context, ViewRenderer... renderers) {
super(viewType, context);
Collections.addAll(mRenderers, renderers);
}
public CompositeViewRenderer registerRenderer(ViewRenderer renderer) {
mRenderers.add(renderer);
return this;
}
public void bindView(M model, VH holder) {}
public VH createViewHolder(ViewGroup parent) { return ...; }
...
}
Здесь я добавил два способа добавления дочерних рендереров, уверен, они нам пригодятся.
Так же обратите внимание на генерик CompositeViewHolder — это будет тоже отдельный класс для композитного ViewHolder, что там будет пока не знаю. А сейчас продолжим работу с CompositeViewRenderer, у нас осталось два обязательных метода — bindView(), createViewHolder(). В createViewHolder() нужно инициализировать адаптер и познакомить его с рендерами, а в bindView() сделаем простое, дефолтное обновление элементов:
public abstract class CompositeViewRenderer <M extends CompositeItemModel, VH extends CompositeViewHolder> extends ViewRenderer<M, VH> {
private final ArrayList<ViewRenderer> mRenderers = new ArrayList<>();
private RendererRecyclerViewAdapter mAdapter;
...
public void bindView(M model, VH holder) {
mAdapter.setItems(model.getItems());
mAdapter.notifyDataSetChanged();
}
public VH createViewHolder(ViewGroup parent) {
mAdapter = new RendererRecyclerViewAdapter();
for (final ViewRenderer renderer : mRenderers) {
mAdapter.registerRenderer(renderer);
}
return ???;
}
...
}
Почти получилось, как оказалось, для такой реализации в методе createViewHolder() нам нужен сам viewHolder, инициализировать мы его тут не можем — создаем отдельный абстрактный метод, заодно хотелось бы тут познакомить наш адаптер с RecyclerView, который мы можем взять у нереализованного CompositeViewHolder — реализуем:
public abstract class CompositeViewHolder extends RecyclerView.ViewHolder {
public RecyclerView mRecyclerView;
public CompositeViewHolder(View itemView) {
super(itemView);
}
}
public abstract class CompositeViewRenderer <M extends CompositeItemModel, VH extends CompositeViewHolder> extends ViewRenderer<M, VH> {
public VH createViewHolder(ViewGroup parent) {
mAdapter = new RendererRecyclerViewAdapter();
for (final ViewRenderer renderer : mRenderers) {
mAdapter.registerRenderer(renderer);
}
VH viewHolder = createCompositeViewHolder(parent);
viewHolder.mRecyclerView.setLayoutManager(createLayoutManager());
viewHolder.mRecyclerView.setAdapter(mAdapter);
return viewHolder;
}
public abstract VH createCompositeViewHolder(ViewGroup parent);
protected RecyclerView.LayoutManager createLayoutManager() {
return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
}
...
}
Да, верно! Я добавил дефолтную реализацию с LinearLayoutManager :( посчитал что это принесет больше пользы, а при необходимости можно метод перегрузить и выставить другой LayoutManager.
Похоже что это все, осталось реализовать конкретные классы и посмотреть что получилось:
public class SomeCompositeItemModel implements CompositeItemModel {
public static final int TYPE = 999;
private int mID;
private final List<ItemModel> mItems;
public SomeCompositeItemModel(final int ID, List<ItemModel> items) {
mID = ID;
mItems = items;
}
public int getID() {
return mID;
}
public int getType() {
return TYPE;
}
public List<ItemModel> getItems() {
return mItems;
}
}
public class SomeCompositeViewHolder extends CompositeViewHolder {
public SomeCompositeViewHolder(View view) {
super(view);
mRecyclerView = (RecyclerView) view.findViewById(R.id.composite_recycler_view);
}
}
public class SomeCompositeViewRenderer extends CompositeViewRenderer<SomeCompositeModel, SomeCompositeViewHolder> {
public SomeCompositeViewRenderer(int viewType, Context context) {
super(viewType, context);
}
public SomeCompositeViewHolder createCompositeViewHolder(ViewGroup parent) {
return new SomeCompositeViewHolder(inflate(R.layout.item_composite, parent));
}
}
Регистрируем наш композитный рендерер:
public class SomeActivity extends AppCompatActivity {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
SomeCompositeViewRenderer composite = new SomeCompositeViewRenderer(
SomeCompositeItemModel.TYPE,
this,
new SomeViewRenderer(SomeModel.TYPE, this, mListener)
);
mRecyclerViewAdapter.registerRenderer(composite);
...
}
...
}
Как видно из последнего семпла, для подписки на клики мы просто передаем необходимый интерфейс в конструктор рендерера, таким образом наше корневое место реализует этот интерфейс и знает о всех необходимых кликах
public class SomeViewRenderer extends ViewRenderer<SomeModel, SomeViewHolder> {
private final Listener mListener;
public SomeViewRenderer(int type, Context context, Listener listener) {
super(type, context);
mListener = listener;
}
public void bindView(SomeModel model, SomeViewHolder holder) {
...
holder.itemView.setOnClickListener(new View.OnClickListener() {
public void onClick(final View view) {
mListener.onSomeItemClicked(model);
}
});
}
...
public interface Listener {
void onSomeItemClicked(SomeModel model);
}
}
Заключение
Мы добились достаточной универсальности и гибкости при работе с вложенными списками, максимально упростили процесс добавления композитных ячеек. Теперь мы легко можем добавлять новые композитные ячейки и легко комбинировать одиночные ячейки во вложенных и основных списках.
Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.
Автор: Виталий Вивчар