Всем привет. Решил поднять такую тему, как сложные списки в андроиде. Эта тема вначале ставила меня в тупик. Это казалось мне очень сложным, но всё на самом деле проще. И думаю, тому, кто сейчас столкнулся с этой проблемой, статья будет полезна.
Все примеры написаны на Kotlin. Я старался везде писать комментарии и максимально понятно сделать для тех, кто пишет на Java.
Итак, вот наша структура классов:
MainActivity
— это основной класс.
ListAdapter
— здесь мы делаем привязки нашего списка к view-элементам.
ViewHolder
(их там несколько штук) — это наша разметка для каждого типа элемента.
Item-классы — это данные для списка (pojo).
Xml-файлы — это разметки (activity_main
для главного экрана, остальные для каждого типа элемента списка).
Первое, что нам надо сделать — добавить зависимость, чтоб мы могли использовать списки (при создании проекта почему-то этой зависимости нету).
В файле build.gradle
в блоке dependencies
добавляем строку:
implementation "androidx.recyclerview:recyclerview:1.1.0"
Дальше находим файл activity_main.xml
:Здесь есть только наш элемент для списков — 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
list_item_google.xml
list_item_apple.xml
Теперь переходим к самому сложному — 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
я наполнил список данными (как вы видите, данные с разными элементами) и передал этот список в адаптер. Дальше запускаем наше приложение, и мы должны увидеть что-то такое на нашем экране:Как видите, в одном списке находятся разные элементы, что и было нашей целью.
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