Делаем код в адаптере чище с помощью MergeAdapter

    Надоели перегруженные и сложные адаптеры в вашем проекте, напоминающие картинку ниже? Каждый раз, при добавлении нового типа ячейки хочется переписать адаптер для RecyclerView, чтобы код читался проще? Есть множество подходов, чаще всего рекомендуется использовать подход delegate adapter или, например библиотеку для динамического создания списков с различными типами view как groupie о работе с которой вы можете ознакомиться в этой статье. Но сегодня расскажем о новом классе, который поможет инкапсулировать логику вашего адаптера для разных ячеек тем самым соответствовать принципам SOLID.

    image

    MergeAdapter — новый класс, появившейся в recyclerview:1.2.0-alpha02, который позволит объединить несколько адаптеров для отображения в едином RecyclerView. Это позволит инкапсулировать логику для каждой ячейки в своём адаптере, и позволит переиспользовать её в будущем.

    Проблема


    Начнем с примера. Предположим, у нас есть задача отобразить ленту с двумя типами данных — текст с описанием и картинка. Код в методе onCreateViewHolder в самом распространённом случае будет выглядеть так:

    override fun onCreateViewHolder(
        parent: ViewGroup, viewType: Int
    ): RecyclerView.ViewHolder? {
        val holder: RecyclerView.ViewHolder
        val inflater = LayoutInflater.from(parent.context)
        when (viewType) {
            TEXT_VIEW_TYPE -> {
                holder = TextViewHolder(
                    inflater.inflate(R.layout.text_item, parent, false)
                )
            }
            IMAGE_VIEW_TYPE -> {
                holder = ImageViewHolder(
                    inflater.inflate(R.layout.image_item, parent, false),
                    imageClickListener
                )
            }
            else -> {
                throw IllegalArgumentException(
                    "Can't create view holder from view type $viewType"
                )
            }
        }
        return holder
    }

    Чем это плохо? Минус такой реализации в нарушении принципов DRY и SOLID (single responsibility и open closed). Чтобы в этом убедиться, достаточно добавить два требования: ввести новый тип данных (чекбокс) и еще одну ленту, где будут только чекбоксы и картинки.

    Перед нами встает выбор — использовать этот же адаптер для второй ленты или создать новый? Независимо от решения, которое мы выберем, нам придется менять код (об одном и том же, но в разных местах). Надо будет добавить новый VIEW_TYPE, новый ViewHolder и отредактировать методы: getItemViewType(), onCreateViewHolder() и onBindViewHolder().

    Если мы решим оставить один адаптер, то на этом изменения закончатся. Но если в будущем новые типы данных с новой логикой будут добавляться только во вторую ленту, первая будет иметь лишний функционал, и ее тоже нужно будет тестировать, хотя она не изменялась.

    Если решим создать новый адаптер, то будет просто масса дублирующего кода.

    Решение


    Новый класс MergeAdapter, позволяет комбинировать разные адаптеры для разного вида ячеек. Например, очень частый кейс использования — отображать spinner на время загрузки данных в ленте, а если, вдруг произошла ошибка загрузки — отображать ячейку с ошибкой в конце ленты.

    image

    Решением данной задачи, может быть использование MergeAdapter Предположим, у нас есть 3 адаптера:

    
    val firstAdapter: FirstAdapter = …
    val secondAdapter: SecondAdapter = …
    val thirdAdapter: ThirdAdapter = …val mergeAdapter = MergeAdapter(firstAdapter, secondAdapter, thirdAdapter)
    recyclerView.adapter = mergeAdapter
    

    RecyclerView отобразит элементы каждого адаптера последовательно, в том же порядке, в котором переданы в конструктор. Разные адаптеры позволяют разделить логику для разных ячеек в списке. Например, если необходимо добавить заголовок к списку, не нужно реализовывать эту логику в адаптере, который отвечает за отображение главного контента в списке, можно разделить адаптеры, для разных видов ячеек. Такой подход помогает инкапсулировать логику и переиспользовать её в будущем для разных экранов.

    image

    Отображение загрузки в заголовке или внизу списка.


    Для отображения статуса загрузки вверху или внизу списка нужно добавить адаптеры соответственно:

    val mergeAdapter = MergeAdapter(headerAdapter, listAdapter, footerAdapter)
    recyclerView.adapter = mergeAdapter

    Верхняя ячейка и нижняя используют тот же layout, ViewHolder и UI логику (отображение статуса загрузки и скрытие). Вообще, достаточно было бы использовать 2 экземпляра одного и того же адаптера для верха и низа списка. Пример можно посмотреть тут или тут.

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

    Понравилась статья? Не забудьте присоединиться к нам в Telegram, а на платформе AndroidSchool.ru публикуются полезные материалы для Android-разработчика и современные туториалы.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 4

      +1

      В разделе "Проблема" ставится задача "Отобразить ленту с двумя типами данных — текст с описанием и картинка", а в разделе "Решение" решается другая задача.

        0
        Здравствуйте, описанная в статье проблема — всего лишь иллюстрация типовых требований к спискам в Android. Если вам нужно реализовать так, как описано в разделе «Проблема» то вы можете используя MergeAdapter создать 2 разных адаптера (один для текста, другой для картинок) разделив тем самым логику.
        0
        слова «не солидно» заиграли новыми красками
          0
          Хочу заметить, что в версии 1.2.0-alpha04 класс переименован в ConcatAdapter

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

          Самое читаемое