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

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

Если Вам знакомы такие проблемы, то добро пожаловать под кат.Из готовых решений нашел 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 для конкретного типа ячейки и зарегистрировать его в нашем адаптере.

На данный момент эта реализация уже положительно себя зарекомендовала в нескольких крупных проектах.

Пример и исходники доступны по ссылке.