Легкая работа со списком - RecyclerView и DiffUtils, RendererRecyclerViewAdapter (часть 2)
В прошлый раз мы оптимизировали работу с RecyclerView, а так же научились переиспользовать ячейки в разных списках и легко добавлять новые. Сегодня мы разберем:
- как можно упростить поддержку DiffUtil в этой реализации;
- как добавить поддержку вложенных RecyclerView.
Если прошлая статья тебе пришлась по душе, думаю, понравится и эта.
DiffUtil
Что такое DiffUtil, я думаю разбирать не стоит. Наверное, уже каждый разработчик опробовал его в своем проекте и получил приятные плюшки в виде анимации и производительности.В первые дни после публикации первой статьи я получил пулл реквест с реализацией DiffUtil, давайте посмотрим как это реализовано. Напомню, что в результате оптимизации у нас получился адаптер с публичным методом setItems(ArrayList <ItemModel> items). В данном виде не очень удобно использовать DiffUtil, нам необходимо где-то дополнительно сохранять старую копию списка, в результате мы получим что-то вроде этого:
MyDiffCallback diffCallback = new MyDiffCallback(getOldItems(), getNewItems());
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.
Похоже что это все, осталось реализовать конкретные классы и посмотреть что получилось:
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);
	}
}Заключение
Мы добились достаточной универсальности и гибкости при работе с вложенными списками, максимально упростили процесс добавления композитных ячеек. Теперь мы легко можем добавлять новые композитные ячейки и легко комбинировать одиночные ячейки во вложенных и основных списках.
Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.