Пользователь Paul Soia
Paul Soia
26 уровень
Киев

Сложные списки — это просто

Статья из группы Android
Всем привет. Решил поднять такую тему, как сложные списки в андроиде. Эта тема вначале ставила меня в тупик. Это казалось мне очень сложным, но всё на самом деле проще. И думаю, тому, кто сейчас столкнулся с этой проблемой, статья будет полезна. Все примеры написаны на Kotlin. Я старался везде писать комментарии и максимально понятно сделать для тех, кто пишет на Java. Итак, вот наша структура классов: Сложные списки - это просто - 1MainActivity — это основной класс. ListAdapter — здесь мы делаем привязки нашего списка к view-элементам. ViewHolder (их там несколько штук) — это наша разметка для каждого типа элемента. Item-классы — это данные для списка (pojo). Xml-файлы — это разметки (activity_main для главного экрана, остальные для каждого типа элемента списка). Первое, что нам надо сделать — добавить зависимость, чтоб мы могли использовать списки (при создании проекта почему-то этой зависимости нету). В файле build.gradle в блоке dependencies добавляем строку:

implementation "androidx.recyclerview:recyclerview:1.1.0"
Дальше находим файл activity_main.xml:Сложные списки - это просто - 2Здесь есть только наш элемент для списков — RecyclerView. Всякие отступы и форматирования добавляем по вкусу. Дальше создаем класс BaseViewHolder. Он будет абстрактным, так как от него будут наследоваться все наши классы ViewHolder. Вполне можно без него обойтись, но всё же рекомендую делать, чтобы все классы-наследники были одинаковыми по структуре. (в Kotlin вместо extends и implement используется :. Как и в Java, тут можно наследоваться от одного класса и реализовать много интерфейсов) Дальше создаем наши дата классы: ItemTitle, ItemGoogle и ItemApple. В Java это обычные классы с конструктором и геттерами.

data class ItemGoogle(
    val name: String,
    val product: String,
    val version: String,
    val isUse: Boolean
) : ListMarker

data class ItemApple(
    val name: String,
    val country: String,
    val year: Int
) : ListMarker

data class ItemTitle(
    val title: String,
    val amount: Int
) : ListMarker
Остановимся тут на интерфейсе ListMarker.

//это наш маркер. Его должны реализовать все айтемы, которые будут
//отображаться в конечном итоге в списке
interface ListMarker
Все наши дата-классы реализовывают этот интерфейс, чтобы привести их к одному типу. Это нам понадобится дальше для нашего списка. Далее создаем наши вьюхолдеры для каждого элемента списка. Не забываем каждый из них наследовать от BaseViewHolder. В этих классах мы задаем, какой элемент view соответствует полю из дата-классов.

abstract class BaseViewHolder<t>(itemView: View) : RecyclerView.ViewHolder(itemView) {
    abstract fun bind(item: T)
}

class GoogleViewHolder(view: View) : BaseViewHolder<itemgoogle>(view) {

    override fun bind(item: ItemGoogle) {
        itemView.tvName.text = item.name
        itemView.tvProduct.text = item.product
        itemView.tvVersion.text = item.version
        if (item.isUse) {
            itemView.tvProduct.visibility = View.GONE
        } else {
            itemView.tvProduct.visibility = View.VISIBLE
        }
    }
}

class AppleViewHolder(view: View) : BaseViewHolder<itemapple>(view) {

    override fun bind(item: ItemApple) {
        //можем делать так
        itemView.tvName.text = item.name
        itemView.tvCountry.text = item.country
        itemView.tvYear.text = item.year.toString()

        /*----сверху и снизу два идентичных блока----*/

        //а можем сделать такой блок и не использовать в каждой строке itemView
        with(itemView) {
            tvName.text = item.name
            tvCountry.text = item.country
            tvYear.text = item.year.toString()
        }
    }
}

class TitleViewHolder(view: View) : BaseViewHolder<itemtitle>(view) {

    override fun bind(item: ItemTitle) {
        itemView.tvTitle.text = item.title
        itemView.tvAmount.text = item.amount.toString()
    }
}
Тут особо никакой логики нету, всё предельно просто. Также своя верстка для каждого вьюхолдера: list_item_title.xmlСложные списки - это просто - 3list_item_google.xmlСложные списки - это просто - 4list_item_apple.xmlСложные списки - это просто - 5Теперь переходим к самому сложному — ListAdapter.

class ListAdapter : RecyclerView.Adapter<baseviewholder<*>>() {

    companion object {
        //задаем константы для каждого типа айтема
        private const val TYPE_TITLE = 0
        private const val TYPE_GOOGLE = 1
        private const val TYPE_APPLE = 2
    }

    //здесь можно использовать обычный ArrayList
    //сюда добавляются все айтемы, которые реализовали интерфейс ListMarker
    //как вариант можно было сделать mutableListOf<any>() и обойтись без интерфейса
    private val items = mutableListOf<listmarker>()

    internal fun swapData(list: List<listmarker>) {
        items.clear()
        items.addAll(list)
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<*> {
        return when(viewType) {
            //задаем разметку для каждого типа айтема
            TYPE_TITLE -> TitleViewHolder(parent.inflate(R.layout.list_item_title))
            TYPE_GOOGLE -> GoogleViewHolder(parent.inflate(R.layout.list_item_google))
            TYPE_APPLE -> AppleViewHolder(parent.inflate(R.layout.list_item_apple))
            else -> throw IllegalArgumentException("Invalid view type")
        }
    }

    override fun getItemViewType(position: Int): Int {
        return when (items[position]) {
            is ItemTitle -> TYPE_TITLE
            is ItemGoogle -> TYPE_GOOGLE
            is ItemApple -> TYPE_APPLE
            else -> throw IllegalArgumentException("Invalid type of item $position")
        }
    }

    override fun getItemCount(): Int {
        //этот метод определяет размер списка
        return items.size
    }

    override fun onBindViewHolder(holder: BaseViewHolder<*>, position: Int) {
        val element = items[position]
        when (holder) {
            //отправляем каждый айтем к своему ViewHolder
            is TitleViewHolder -> holder.bind(element as ItemTitle)
            is GoogleViewHolder -> holder.bind(element as ItemGoogle)
            is AppleViewHolder -> holder.bind(element as ItemApple)
            else -> throw IllegalArgumentException()
        }
    }
}
</listmarker></listmarker></any></baseviewholder<*>
Когда мы наследуем наш класс от RecyclerView.Adapter, то надо будет переопределить три метода: onCreateViewHolder, getItemCount, onBindViewHolder. onCreateViewHolder — здесь мы инициализируем наши классы ViewHolder. getItemCount — этот метод отвечает за размер списка. onBindViewHolder — здесь мы передаем элементы списка в наши классы ViewHolder. Для обычного списка с одним типом элемента этого было бы достаточно. Но для разных типов надо еще переопределить метод getItemViewType (для этого используем константы, которые есть в верху нашего класса-адаптера. В Java можно использовать final переменные для этого). Также в Java в методе onBindViewHolder вместо выражения when можно использовать обычный if. И, наконец-то, переходим к нашему главному классу — MainActivity.

class MainActivity : AppCompatActivity() {

    private val adapter = ListAdapter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initRecyclerView()
    }

    private fun initRecyclerView() {
        rvList.layoutManager = LinearLayoutManager(this)

        //здесь мы задаем разделитель между айтемами, чтоб они не сливались друг с другом
        val divider = DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
        rvList.addItemDecoration(divider)

        rvList.adapter = adapter
        stubData()
    }

    private fun stubData() {
        val list = mutableListOf<listmarker>()
        list.add(ItemTitle("title1", 4))
        list.add(ItemGoogle("android", "product1", "17.0v", true))
        list.add(ItemGoogle("no name", "product2", "3.1v", false))
        list.add(ItemApple("macOs", "USA", 2005))
        list.add(ItemApple("iOs", "China", 2007))
        list.add(ItemTitle("title2", 2))
        list.add(ItemGoogle("map", "product3", "23.0v", true))
        list.add(ItemApple("car", "England", 2018))
        list.add(ItemTitle("title3", 0))
        //отправляем все данные в адаптер
        adapter.swapData(list)
    }
}
</listmarker>
Здесь происходит инициализация нашего RecyclerView и ListAdapter. Это довольно таки шаблонный код, с которым многие сталкивались. В методе stubData я наполнил список данными (как вы видите, данные с разными элементами) и передал этот список в адаптер. Дальше запускаем наше приложение, и мы должны увидеть что-то такое на нашем экране:Сложные списки - это просто - 6Как видите, в одном списке находятся разные элементы, что и было нашей целью. P.S. Забыл упомянуть о файле Extension. Вот так он выглядит:

//это расширение для класса ViewGroup. Теперь мы можем по всему проекту использовать
//короткое inflate вместо длинного LayoutInflater.from(context).inflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View =
    LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
Это не класс, а файл, потому и нету названия внутри него. Зато теперь мы можем использовать в адаптере просто inflate вместо длинной конструкции. К сожалению, для Java это не предусмотрено, потому пишите LayoutInflater.from(context).inflate. На этом всё. До следующих встреч :) Ссылка на GitHub
Комментарии (1)
Чтобы просмотреть все комментарии или оставить свой,
перейдите в полную версию
Хорс 41 уровень, Харьков
22 января 2021
Спасибо тебе большущее!!! Я уже неделю бился над пониманием этого RecyclerView. Хоть немного начал понимать сейчас. Код в тексте немного кривой, а вот код с гитхаба вполне рабочий!!!