Всім привіт. Вирішив порушити таку тему, як складні списки в андроїді. Ця тема спочатку ставила мене в глухий кут. Це здавалося мені дуже складним, але насправді все простіше. І гадаю, тому, хто зараз зіткнувся з цією проблемою, стаття буде корисною. Усі приклади написані на 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
я заповнив список даними (як ви бачите, дані з різними елементами) і передав цей список адаптер. Далі запускаємо нашу програму, і ми повинні побачити щось таке на нашому екрані: Як бачите, в одному списку знаходяться різні елементи, що й було нашою метою. PS Забув згадати про файл 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
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ