Поначалу я как обычно выносил все лишнее из адаптеров в презентеры, фрагменты и другие классы. В итоге я пришел к мнению, почему бы не:
- «обезопасить» свои адаптеры от внесения туда лишней логики;
- переиспользовать биндинги ячеек;
- добиться какой-то универсальности для работы с несколькими типами ячеек.
Если Вам знакомы такие проблемы, то добро пожаловать под кат.
Из готовых решений нашел AdapterDelegates, но он не подошел мне по первому условию.
Требования
Для начала я выписал несколько уже сформированных требований:
- работа с RecyclerView без реализации нового адаптера;
- возможность переиспользовать ячейки в другом RecyclerView;
- простое добавление других типов ячеек в RecyclerView.
Реализация
Первым делом я посмотрел что я всегда делаю в адаптере, для этого создал тестовую реализацию и проанализировал использованные мной методы:
public
class Test extends RecyclerView.Adapter
{
	@Override
	public
        ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
	}
	@Override
	public
	void onBindViewHolder(final ViewHolder holder, final int position) {
	}
	@Override
	public
	int getItemCount() {
		return 0;
	}
	public
	void setItems(@NonNull final ArrayList items) {
	}
}
Всего-ничего получилось 4 метода. Сразу в глаза бросается метод setItems(), он должен уметь принимать разные списки моделей, создаем пустой интерфейс и обновляем код в тестовом адаптере:
public
interface ItemModel
{
}
public
class Test extends RecyclerView.Adapter
{
        @NonNull
	private final ArrayList<ItemModel> mItems = new ArrayList<>();
        ....
	@Override
	public
	int getItemCount() {
                return mItems.size();
	}
        public
	void setItems(@NonNull final ArrayList<ItemModel> items) {
		mItems.clear();
		mItems.addAll(items);
	}
}
Теперь нужно что-то придумать с onCreateViewHolder() и onBindViewHolder().
Если я хочу чтобы адаптер мог биндить разные вьюхи, то лучше если он будет это кому-то делегировать. И это позволит потом переиспользовать реализацию. Создаем абстрактный класс, который будет уметь работать только с одним типом ячеек и, конечно же, с определенным ViewHolder'ом. Для этого используем генерики чтобы избежать кастов. Назовем его ViewRenderer — больше ничего толкого в голову не пришло.
public
abstract
class ViewRenderer <M extends ItemModel, VH extends RecyclerView.ViewHolder>
{
	public abstract
	void bindView(@NonNull M model, @NonNull VH holder);
	@NonNull
	public abstract
	VH createViewHolder(@Nullable ViewGroup parent);
}
Попробуем использовать его в нашем адаптере. Переименуем адаптер в что-то осмысленное и доработаем код:
public
class RendererRecyclerViewAdapter extends RecyclerView.Adapter
{
        ...
        private ViewRenderer mRenderer;
	@Override
	public
	RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
		return mRenderer.createViewHolder(parent);
	}
	@Override
	public
	void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
		mRenderer.bindView(item, holder);
	}
        public 
        void registerRenderer(@NonNull final ViewRenderer renderer) {
                mRenderer = renderer;
	}
        ...
}
Выглядит пока все неплохо. Но наш адаптер должен уметь работать с несколькими типами вьюх. Для этого у адаптера есть метод getItemViewType(), оверрайдим его в нашем адаптере.
И попробуем спрашивать тип ячейки у самой модели — добавим метод в интерфейс и обновим метод адаптера:
public
interface ItemModel
{
	int getType();
}
public
class RendererRecyclerViewAdapter extends RecyclerView.Adapter
{
        ...
	@Override
	public
	int getItemViewType(final int position) {
		final ItemModel item = getItem(position);
		return item.getType();
	}
	private
	ItemModel getItem(final int position) {
		return mItems.get(position);
	}
        ...
}
Заодно доработаем поддержку нескольких ViewRenderer'ов:
public
class RendererRecyclerViewAdapter extends RecyclerView.Adapter
{
        ...
	@NonNull
	private final SparseArray<ViewRenderer> mRenderers = new SparseArray<>();
	@Override
	public
	RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
		final ViewRenderer renderer = mRenderers.get(viewType);
		if (renderer != null) {
			return renderer.createViewHolder(parent);
		}
		throw new RuntimeException("Not supported Item View Type: " + viewType);
	}
	public
	void registerRenderer(@NonNull final ViewRenderer renderer) {
		final int type = renderer.getType();
		if (mRenderers.get(type) == null) {
			mRenderers.put(type, renderer);
		} else {
			throw new RuntimeException("ViewRenderer already exist with this type: " + type);
		}
	}
	@SuppressWarnings("unchecked")
	@Override
	public
	void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
		final ItemModel item = getItem(position);
		final ViewRenderer renderer = mRenderers.get(item.getType());
		if (renderer != null) {
			renderer.bindView(item, holder);
		} else {
			throw new RuntimeException("Not supported View Holder: " + holder);
		}
	}
        ...
}
Как мы видим у рендерера появился метод getType(), это нужно чтобы найти необходимый рендерер для конкретной вьюхи.
Наш адаптер готов.
Реализуем конкретные классы ItemModel, ViewHolder, ViewRenderer:
public
class SomeModel implements ItemModel
{
    public static final int TYPE = 0;
    @NonNull
    private final String mTitle;
    public
    SomeModel(@NonNull final String title) {
        mTitle = title;
    }
    @Override
    public
    int getType() {
        return TYPE;
    }
    @NonNull
    public
    String getTitle() {
        return mTitle;
    }
    ...
}
public
class SomeViewHolder
        extends RecyclerView.ViewHolder
{
    public final TextView mTitle;
    public
    SomeViewHolder(final View itemView) {
        super(itemView);
        mTitle = (TextView) itemView.findViewById(R.id.title);
        ...
    }
}
public
class SomeViewRenderer
        extends ViewRenderer<SomeModel, SomeViewHolder>
{
    public
    SomeViewRenderer(final int type, final Context context) {
        super(type, context);
    }
    @Override
    public
    void bindView(@NonNull final SomeModel model, @NonNull final SomeViewHolder holder) {
        ...
    }
    @NonNull
    @Override
    public
    SomeViewHolder createViewHolder(@Nullable final ViewGroup parent) {
        return new SomeViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.some_item, parent, false));
    }
}
У ViewRender'а появился конструктор и два параметра для него — ViewRenderer(int viewType, Context context), для чего это нужно, думаю, пояснять не нужно.
Теперь можно знакомить наш адаптер с RecyclerView:
public
class SomeActivity
        extends AppCompatActivity
{
    private RendererRecyclerViewAdapter mRecyclerViewAdapter;
    private RecyclerView mRecyclerView;
    @Override
    protected
    void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        mRecyclerViewAdapter = new RendererRecyclerViewAdapter();
        mRecyclerViewAdapter.registerRenderer(new SomeViewRenderer(SomeModel.TYPE, this));
//        mRecyclerViewAdapter.registerRenderer(...); 
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setAdapter(mRecyclerViewAdapter);
        mRecyclerViewAdapter.setItems(getItems());
        mRecyclerViewAdapter.notifyDataSetChanged();
    }
    ...
}
Заключение
Достаточно небольшими силами мы получили рабочую версию адаптера, которую можно легко использовать с несколькими типами ячеек, для этого достаточно реализовать ViewRenderer для конкретного типа ячейки и зарегистрировать его в нашем адаптере.
На данный момент эта реализация уже положительно себя зарекомендовала в нескольких крупных проектах.
Пример и исходники доступны по ссылке.
Комментарии (10)
 - evstep14.03.2017 15:50- А почему нельзя поместить mRecyclerViewAdapter.notifyDataSetChanged(); внутри setItems()?  - vivchar14.03.2017 19:29- можно, но зачем? к примеру: мы меняем массив в презентере и хотим вызвать не notifyDataSetChanged(), a notifyItemRangeInserted()  - Mujahit15.03.2017 04:47- А если вместо notifyDataSetChanged() заюзать DiffUtil?  - vivchar16.03.2017 19:14- DiffUtil работает с конкретной реализацией YourModelCallbak, а в адаптере мы работаем только с интерфейсом — ItemModel, конечно это можно сделать через интерфейс, но тогда все обязаны будут работать так — это ограничивает реализацию. В данном случае выбор остается за разработчиком, как оповещать адаптер о изменениях. 
 DiffUtil можно использовать извне адаптера.
 
 
 
 - nadstas15.03.2017 06:55- А что если я хочу добавлять элементы по одному? Например что бы анимировать их добавление. Может стоит добавить методы добавления и удаления одного элемента и внести notifyDataSetChanged/notifyItemInserted и т.д. внутрь адаптера?  - AlexeyKorshun16.03.2017 09:50- для этого можно поставить новый список со всеми элементами и вызвать - notifyItemInsered(int position)
 
 
           
 
sergeyfitis
Похоже на вариацию паттерна DelegateAdapter от Juan Ignacio в статье RecyclerView?—?Delegate Adapters
sergeyfitis
и да, DelegateAdapter от Juan Ignacio тоже основана та статье Hannes Dorfmann про Adapter Delegates(JOE'S GREAT ADAPTER HELL ESCAPE).