В последнее время мне часто приходилось переписывать много адаптеров для списков, и каждый раз я брался за голову — в адаптере находилась бизнес-логика, сетевые запросы и роутинг приложения и многое другое. Все это очень сложно поддавалось изменениям.
Поначалу я как обычно выносил все лишнее из адаптеров в презентеры, фрагменты и другие классы. В итоге я пришел к мнению, почему бы не:
Если Вам знакомы такие проблемы, то добро пожаловать под кат.
Из готовых решений нашел AdapterDelegates, но он не подошел мне по первому условию.
Для начала я выписал несколько уже сформированных требований:
Первым делом я посмотрел что я всегда делаю в адаптере, для этого создал тестовую реализацию и проанализировал использованные мной методы:
Всего-ничего получилось 4 метода. Сразу в глаза бросается метод setItems(), он должен уметь принимать разные списки моделей, создаем пустой интерфейс и обновляем код в тестовом адаптере:
Теперь нужно что-то придумать с onCreateViewHolder() и onBindViewHolder().
Если я хочу чтобы адаптер мог биндить разные вьюхи, то лучше если он будет это кому-то делегировать. И это позволит потом переиспользовать реализацию. Создаем абстрактный класс, который будет уметь работать только с одним типом ячеек и, конечно же, с определенным ViewHolder'ом. Для этого используем генерики чтобы избежать кастов. Назовем его ViewRenderer — больше ничего толкого в голову не пришло.
Попробуем использовать его в нашем адаптере. Переименуем адаптер в что-то осмысленное и доработаем код:
Выглядит пока все неплохо. Но наш адаптер должен уметь работать с несколькими типами вьюх. Для этого у адаптера есть метод getItemViewType(), оверрайдим его в нашем адаптере.
И попробуем спрашивать тип ячейки у самой модели — добавим метод в интерфейс и обновим метод адаптера:
Заодно доработаем поддержку нескольких ViewRenderer'ов:
Как мы видим у рендерера появился метод getType(), это нужно чтобы найти необходимый рендерер для конкретной вьюхи.
Наш адаптер готов.
Реализуем конкретные классы ItemModel, ViewHolder, ViewRenderer:
У ViewRender'а появился конструктор и два параметра для него — ViewRenderer(int viewType, Context context), для чего это нужно, думаю, пояснять не нужно.
Теперь можно знакомить наш адаптер с RecyclerView:
Достаточно небольшими силами мы получили рабочую версию адаптера, которую можно легко использовать с несколькими типами ячеек, для этого достаточно реализовать ViewRenderer для конкретного типа ячейки и зарегистрировать его в нашем адаптере.
На данный момент эта реализация уже положительно себя зарекомендовала в нескольких крупных проектах.
Пример и исходники доступны по ссылке.
UPD: Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)
Поначалу я как обычно выносил все лишнее из адаптеров в презентеры, фрагменты и другие классы. В итоге я пришел к мнению, почему бы не:
- «обезопасить» свои адаптеры от внесения туда лишней логики;
- переиспользовать биндинги ячеек;
- добиться какой-то универсальности для работы с несколькими типами ячеек.
Если Вам знакомы такие проблемы, то добро пожаловать под кат.
Из готовых решений нашел 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 для конкретного типа ячейки и зарегистрировать его в нашем адаптере.
На данный момент эта реализация уже положительно себя зарекомендовала в нескольких крупных проектах.
Пример и исходники доступны по ссылке.
UPD: Легкая работа со списками — RendererRecyclerViewAdapter (часть 2)
