В прошлый раз мы оптимизировали работу с RecyclerView, а так же научились переиспользовать ячейки в разных списках и легко добавлять новые.
Сегодня мы разберем:
Если прошлая статья тебе пришлась по душе, думаю, понравится и эта.
Что такое DiffUtil, я думаю разбирать не стоит. Наверное, уже каждый разработчик опробовал его в своем проекте и получил приятные плюшки в виде анимации и производительности.
В первые дни после публикации первой статьи я получил пулл реквест с реализацией DiffUtil, давайте посмотрим как это реализовано. Напомню, что в результате оптимизации у нас получился адаптер с публичным методом setItems(ArrayList <ItemModel> items). В данном виде не очень удобно использовать DiffUtil, нам необходимо где-то дополнительно сохранять старую копию списка, в результате мы получим что-то вроде этого:
И расширенный интерфейс ItemModel:
В общем-то реализуемо и не сложно, но если это делать в нескольких местах, то стоит задуматься зачем столько много одинакового кода. Попробуем вынести общие моменты в свою реализацию DiffUtil.Callback:
В общем получилось достаточно универсально, мы избавились от рутинны и сосредоточились на главных методах — areItemsTheSame() и areContentsTheSame(), которые обязательны к реализации и могут отличаться.
Реализация метода getChangePayload() намеренно пропущена, её реализацию можно посмотреть в исходниках.
Теперь мы можем добавить еще один метод с поддержкой DiffUtil в наш адаптер:
В общем то с DiffUtil это все, теперь при необходимости мы используем наш абстрактный класс — DiffCallback, и реализуем всего два метода.
Я думаю теперь мы разогрелись и освежили память, значит, можно перейти к более интересным вещам.
Часто по воле заказчика или веянию дизайнеров в нашем приложении появляются вложенные списки. До недавних пор я недолюбливал их, я сталкивался с такими проблемами:
Некоторые из этих проблем сомнительны и легко решаемы, а некоторые уйдут, если подключить наш оптимизированный адаптер из первой статьи :). Но, как минимум, сложность реализации у нас останется. Давайте сформулируем наши требования:
Важно заметить, что здесь я разделил понятие ячейка и элемент списка:
элемент списка — сущность используемая в RecyclerView.
ячейка — набор классов, позволяющих отобразить один тип элемента списка, в нашем случае это реализация ранее известных классов и интерфейсов: ViewRenderer, ItemModel, ViewHolder.
И так, что мы имеем. Ключевым интерфесом у нас является ItemModel, очевидно что нам удобно будет далее с ним и работать. Наша композитная модель должна содержать в себе дочерние модели, добавляем новый интерфейс:
Выглядит неплохо, соответсвенно, композитный ViewRenderer должен знать о дочерних рендерерах — добавляем:
Здесь я добавил два способа добавления дочерних рендереров, уверен, они нам пригодятся.
Так же обратите внимание на генерик CompositeViewHolder — это будет тоже отдельный класс для композитного ViewHolder, что там будет пока не знаю. А сейчас продолжим работу с CompositeViewRenderer, у нас осталось два обязательных метода — bindView(), createViewHolder(). В createViewHolder() нужно инициализировать адаптер и познакомить его с рендерами, а в bindView() сделаем простое, дефолтное обновление элементов:
Почти получилось, как оказалось, для такой реализации в методе createViewHolder() нам нужен сам viewHolder, инициализировать мы его тут не можем — создаем отдельный абстрактный метод, заодно хотелось бы тут познакомить наш адаптер с RecyclerView, который мы можем взять у нереализованного CompositeViewHolder — реализуем:
Да, верно! Я добавил дефолтную реализацию с LinearLayoutManager :( посчитал что это принесет больше пользы, а при необходимости можно метод перегрузить и выставить другой LayoutManager.
Похоже что это все, осталось реализовать конкретные классы и посмотреть что получилось:
Регистрируем наш композитный рендерер:
Как видно из последнего семпла, для подписки на клики мы просто передаем необходимый интерфейс в конструктор рендерера, таким образом наше корневое место реализует этот интерфейс и знает о всех необходимых кликах
Заключение
Мы добились достаточной универсальности и гибкости при работе с вложенными списками, максимально упростили процесс добавления композитных ячеек. Теперь мы легко можем добавлять новые композитные ячейки и легко комбинировать одиночные ячейки во вложенных и основных списках.
Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.
Сегодня мы разберем:
- как можно упростить поддержку 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); ...
Классическая реализация DiffUtil.Callback
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.
Похоже что это все, осталось реализовать конкретные классы и посмотреть что получилось:
SomeCompositeItemModel
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; } }
SomeCompositeViewHolder
public class SomeCompositeViewHolder extends CompositeViewHolder { public SomeCompositeViewHolder(View view) { super(view); mRecyclerView = (RecyclerView) view.findViewById(R.id.composite_recycler_view); } }
SomeCompositeViewRenderer
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); } }
Заключение
Мы добились достаточной универсальности и гибкости при работе с вложенными списками, максимально упростили процесс добавления композитных ячеек. Теперь мы легко можем добавлять новые композитные ячейки и легко комбинировать одиночные ячейки во вложенных и основных списках.
Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.
