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

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


    Заключение

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

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

    Подробнее
    Реклама
    Комментарии 0

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