Легкая работа со списком - RecyclerView пример оптимизации, RendererRecyclerViewAdapter (часть 1)
В последнее время мне часто приходилось переписывать много адаптеров для списков, и каждый раз я брался за голову — в адаптере находилась бизнес-логика, сетевые запросы и роутинг приложения и многое другое. Все это очень сложно поддавалось изменениям.Поначалу я как обычно выносил все лишнее из адаптеров в презентеры, фрагменты и другие классы. В итоге я пришел к мнению, почему бы не:
- «обезопасить» свои адаптеры от внесения туда лишней логики;
- переиспользовать биндинги ячеек;
- добиться какой-то универсальности для работы с несколькими типами ячеек.
Если Вам знакомы такие проблемы, то добро пожаловать под кат.Из готовых решений нашел AdapterDelegates, но он не подошел мне по первому условию.
Требования
Для начала я выписал несколько уже сформированных требований:
- работа с RecyclerView без реализации нового адаптера;
- возможность переиспользовать ячейки в другом RecyclerView;
- простое добавление других типов ячеек в RecyclerView.
Реализация
Первым делом я посмотрел что я всегда делаю в адаптере, для этого создал тестовую реализацию и проанализировал использованные мной методы:
public class
Test extends RecyclerView.Adapter {
@Override
public ViewHolder
onCreateViewHolder(ViewGroup parent, int viewType) {
}
@Override
public void
onBindViewHolder(ViewHolder holder, int position) {
}
@Override
public int
getItemCount() {
return 0;
}
public void
setItems(ArrayList items) {
}
}
Всего-ничего получилось 4 метода. Сразу в глаза бросается метод setItems()
, он должен уметь принимать разные списки моделей, создаем пустой интерфейс и обновляем код в тестовом адаптере:
public interface ItemModel {}
public class Test extends RecyclerView.Adapter {
private ArrayList<ItemModel> mItems = new ArrayList<>();
....
@Override
public int
getItemCount() {
return mItems.size();
}
public void
setItems(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(M model, VH holder);
public abstract VH createViewHolder(ViewGroup parent);
}
Попробуем использовать его в нашем адаптере. Переименуем адаптер в что-то осмысленное и доработаем код:
public class RendererRecyclerViewAdapter extends RecyclerView.Adapter {
...
private ViewRenderer mRenderer;
@Override
public RecyclerView.ViewHolder
onCreateViewHolder(ViewGroup parent, int viewType) {
return mRenderer.createViewHolder(parent);
}
@Override
public void
onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
mRenderer.bindView(item, holder);
}
public void
registerRenderer(ViewRenderer renderer) {
mRenderer = renderer;
}
...
}
Выглядит пока все неплохо. Но наш адаптер должен уметь работать с несколькими типами вьюх. Для этого у адаптера есть метод getItemViewType()
, оверрайдим его в нашем адаптере.
И попробуем спрашивать тип ячейки у самой модели — добавим метод в интерфейс и обновим метод адаптера:
public interface
ItemModel {
int getType();
}
public class
RendererRecyclerViewAdapter extends RecyclerView.Adapter {
...
@Override
public int
getItemViewType(int position) {
ItemModel item = getItem(position);
return item.getType();
}
private ItemModel
getItem(int position) {
return mItems.get(position);
}
...
}
Заодно доработаем поддержку нескольких ViewRenderer'ов:
public class
RendererRecyclerViewAdapter extends RecyclerView.Adapter {
...
@NonNull
private SparseArray<ViewRenderer> mRenderers = new SparseArray<>();
@Override
public RecyclerView.ViewHolder
onCreateViewHolder(ViewGroup parent, int viewType) {
ViewRenderer renderer = mRenderers.get(viewType);
if (renderer != null) {
return renderer.createViewHolder(parent);
}
throw new RuntimeException("Not supported Item View Type: " + viewType);
}
public void
registerRenderer(ViewRenderer renderer) {
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(RecyclerView.ViewHolder holder, int position) {
ItemModel item = getItem(position);
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
, VxiewRenderer
:
public class
SomeModel implements ItemModel {
public static int TYPE = 0;
private String mTitle;
public
SomeModel(String title) {
mTitle = title;
}
@Override
public
int getType() {
return TYPE;
}
public
String getTitle() {
return mTitle;
}
...
}
public class
SomeViewHolder
extends RecyclerView.ViewHolder {
public TextView mTitle;
public
SomeViewHolder(View itemView) {
super(itemView);
mTitle = (TextView) itemView.findViewById(R.id.title);
...
}
}
public class
SomeViewRenderer
extends ViewRenderer<SomeModel, SomeViewHolder> {
public
SomeViewRenderer(int type, Context context) {
super(type, context);
}
@Override
public void
bindView(SomeModel model, SomeViewHolder holder) {
...
}
@NonNull
@Override
public SomeViewHolder
createViewHolder(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(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 для конкретного типа ячейки и зарегистрировать его в нашем адаптере.
На данный момент эта реализация уже положительно себя зарекомендовала в нескольких крупных проектах.
Пример и исходники доступны по ссылке.