В последнее время мне часто приходилось переписывать много адаптеров для списков, и каждый раз я брался за голову — в адаптере находилась бизнес-логика, сетевые запросы и роутинг приложения и многое другое. Все это очень сложно поддавалось изменениям.
Поначалу я как обычно выносил все лишнее из адаптеров в презентеры, фрагменты и другие классы. В итоге я пришел к мнению, почему бы не:
- «обезопасить» свои адаптеры от внесения туда лишней логики;
- переиспользовать биндинги ячеек;
- добиться какой-то универсальности для работы с несколькими типами ячеек.
Если Вам знакомы такие проблемы, то добро пожаловать под кат.
Из готовых решений нашел 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 для конкретного типа ячейки и зарегистрировать его в нашем адаптере.
На данный момент эта реализация уже положительно себя зарекомендовала в нескольких крупных проектах.
Пример и исходники доступны по ссылке.
Автор: adon1s