Легкая работа со списками — RendererRecyclerViewAdapter

В последнее время мне часто приходилось переписывать много адаптеров для списков, и каждый раз я брался за голову — в адаптере находилась бизнес-логика, сетевые запросы и роутинг приложения и многое другое. Все это очень сложно поддавалось изменениям.

Поначалу я как обычно выносил все лишнее из адаптеров в презентеры, фрагменты и другие классы. В итоге я пришел к мнению, почему бы не:

  1. «обезопасить» свои адаптеры от внесения туда лишней логики;
  2. переиспользовать биндинги ячеек;
  3. добиться какой-то универсальности для работы с несколькими типами ячеек.

Если Вам знакомы такие проблемы, то добро пожаловать под кат.

Из готовых решений нашел 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:

SomeModel
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;
    }
    ...
}


SomeViewHolder
public
class SomeViewHolder
        extends RecyclerView.ViewHolder
{

    public final TextView mTitle;

    public
    SomeViewHolder(final View itemView) {
        super(itemView);
        mTitle = (TextView) itemView.findViewById(R.id.title);
        ...
    }
}


SomeViewRenderer
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
  • 0

    Похоже на вариацию паттерна DelegateAdapter от Juan Ignacio в статье RecyclerView — Delegate Adapters

    • 0

      и да, DelegateAdapter от Juan Ignacio тоже основана та статье Hannes Dorfmann про Adapter Delegates(JOE'S GREAT ADAPTER HELL ESCAPE).

    • 0
      А почему нельзя поместить mRecyclerViewAdapter.notifyDataSetChanged(); внутри setItems()?
      • 0
        можно, но зачем? к примеру: мы меняем массив в презентере и хотим вызвать не notifyDataSetChanged(), a notifyItemRangeInserted()
        • 0

          А если вместо notifyDataSetChanged() заюзать DiffUtil?

          • 0
            DiffUtil работает с конкретной реализацией YourModelCallbak, а в адаптере мы работаем только с интерфейсом — ItemModel, конечно это можно сделать через интерфейс, но тогда все обязаны будут работать так — это ограничивает реализацию. В данном случае выбор остается за разработчиком, как оповещать адаптер о изменениях.
            DiffUtil можно использовать извне адаптера.
      • +1
        AAAAA, вырвиглазное форматирование. Думал из-за хабра, но по ссылке на гитхабе тот же стайл.
        вы правда
        код так пишете?
        • 0
          Очень конструктивно. Да, мы так пишем, это утвержденный кодестайл внутри рабочей группы.
        • 0
          А что если я хочу добавлять элементы по одному? Например что бы анимировать их добавление. Может стоит добавить методы добавления и удаления одного элемента и внести notifyDataSetChanged/notifyItemInserted и т.д. внутрь адаптера?
          • 0
            для этого можно поставить новый список со всеми элементами и вызвать
            notifyItemInsered(int position)
            

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