Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)

в 3:28, , рубрики: adapter, android, android development, composite, nested recyclerview, recyclerview, разработка мобильных приложений, Разработка под android

В прошлый раз мы оптимизировали работу с 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);
        ...

Классическая реализация 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);
	}
}

Заключение

Мы добились достаточной универсальности и гибкости при работе с вложенными списками, максимально упростили процесс добавления композитных ячеек. Теперь мы легко можем добавлять новые композитные ячейки и легко комбинировать одиночные ячейки во вложенных и основных списках.

Демонстрация, более детальная реализация и решения некоторых проблем доступны по ссылке.

Автор: Виталий Вивчар

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js