Cześć wszystkim. Postanowiłem poruszyć temat złożonych list w Androidzie. Na początku ten temat mnie zaintrygował. Wydawało mi się to bardzo trudne, ale tak naprawdę wszystko jest prostsze. Myślę, że każdy, kto stanie teraz przed tym problemem, uzna ten artykuł za przydatny. Wszystkie przykłady są napisane w Kotlinie. Starałem się pisać komentarze wszędzie i uczynić to tak jasnym, jak to możliwe dla tych, którzy piszą w Javie. Oto nasza struktura klas:
MainActivity
- To jest klasa główna. ListAdapter
- tutaj wiążemy naszą listę z elementami widoku. ViewHolder
(jest ich kilka) - to jest nasz znacznik dla każdego typu elementu. Klasy elementów są danymi dla listy (pojo). Pliki Xml to znaczniki ( activity_main
dla ekranu głównego, reszta dla każdego typu elementu listy). Pierwszą rzeczą, którą musimy zrobić, to dodać zależność, abyśmy mogli korzystać z list (z jakiegoś powodu tej zależności brakuje podczas tworzenia projektu). W pliku build.gradle
w bloku dependencies
dodajemy linię:
implementation "androidx.recyclerview:recyclerview:1.1.0"
Następnie znajdujemy plik activity_main.xml
: Tutaj znajduje się tylko nasz element dla list - RecyclerView
. Dodajemy dowolne wcięcia i formatowanie według własnego uznania. Następnie tworzymy klasę BaseViewHolder
. Będzie to abstrakcyjne, ponieważ wszystkie nasze klasy będą po nim dziedziczyć ViewHolder
. Można się bez tego obejść, ale nadal zalecam upewnienie się, że wszystkie klasy dziedziczące mają tę samą strukturę. (w Kotlinie extends
i zamiast tego implement
używane jest :
. Podobnie jak w Javie, tutaj można dziedziczyć z jednej klasy i implementować wiele interfejsów) Następnie tworzymy nasze klasy danych: ItemTitle
, ItemGoogle
i ItemApple
. W Javie są to zwykłe klasy z konstruktorem i modułami pobierającymi.
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
Skupmy się tutaj na interfejsie ListMarker
.
//это наш маркер. Его должны реализовать все айтемы, которые будут
//отображаться в конечном итоге в списке
interface ListMarker
Wszystkie nasze klasy danych implementują ten interfejs, aby rzutować je na ten sam typ. Będziemy potrzebować tego później do naszej listy. Następnie tworzymy naszych widzów dla każdego elementu listy. Nie zapominaj, że każdy z nich dziedziczy po BaseViewHolder
. W tych klasach określamy, który element view
odpowiada polu z klas danych.
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()
}
}
Nie ma tu żadnej szczególnej logiki, wszystko jest niezwykle proste. Ponadto każdy widz ma swój własny układ: list_item_title.xml
list_item_google.xml
list_item_apple.xml
teraz przejdźmy do najtrudniejszej części - 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
//Jak вариант можно было сделать 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<*>
Kiedy dziedziczymy naszą klasę z RecyclerView.Adapter
, będziemy musieli zastąpić trzy metody: onCreateViewHolder
, getItemCount
, onBindViewHolder
. onCreateViewHolder
- tutaj inicjujemy nasze klasy ViewHolder
. getItemCount
— ta metoda odpowiada za rozmiar listy. onBindViewHolder
- tutaj przekazujemy elementy listy naszym klasom ViewHolder
. W przypadku zwykłej listy z jednym typem elementu to by wystarczyło. Jednak w przypadku różnych typów nadal musimy przedefiniować metodę getItemViewType
(w tym celu używamy stałych, które znajdują się na górze naszej klasy adaptera. W Javie można final
do tego używać zmiennych). Również w Javie onBindViewHolder
zamiast wyrażenia when
można użyć zwykłego if
. I na koniec przejdźmy do naszej głównej klasy - 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>
W tym miejscu inicjowane jest nasze RecyclerView
i ListAdapter
. Jest to ładny szablonowy kod, z którym wielu się zetknęło. W metodzie stubData
wypełniłem listę danymi (jak widać danymi różnymi elementami) i przekazałem tę listę do adaptera. Następnie uruchamiamy naszą aplikację i na ekranie powinniśmy zobaczyć coś takiego: Jak widać na jednej liście znajdują się różne elementy, co było naszym celem. PS Zapomniałem wspomnieć o Extension
. Oto jak to wygląda:
//это расширение для класса ViewGroup. Теперь мы можем по всему проекту использовать
//короткое inflate zamiast длинного LayoutInflater.from(context).inflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
To nie jest klasa, ale plik, dlatego nie ma w nim nazwy. Ale teraz możemy go używać w adapterze po prostu inflate
zamiast długiej konstrukcji. Niestety nie jest to dostępne dla Java, więc napisz LayoutInflater.from(context).inflate
. To wszystko. Do następnego razu :) Link do GitHuba
GO TO FULL VERSION