Hi all. I decided to raise the topic of complex lists in Android. This topic puzzled me at first. It seemed very difficult to me, but everything is actually simpler. And I think that anyone who is now faced with this problem will find the article useful. All examples are written in Kotlin. I tried to write comments everywhere and make it as clear as possible for those who write in Java. So here is our class structure:
MainActivity
- This is the main class. ListAdapter
- here we bind our list to view elements. ViewHolder
(there are several of them) - this is our markup for each type of element. Item classes are data for a list (pojo). Xml files are markups ( activity_main
for the main screen, the rest for each type of list element). The first thing we need to do is add a dependency so that we can use lists (for some reason this dependency is missing when creating a project). In the file build.gradle
in the block dependencies
we add the line:
implementation "androidx.recyclerview:recyclerview:1.1.0"
Next we find the file activity_main.xml
: Here there is only our element for lists - RecyclerView
. We add any indents and formatting to taste. Next we create the class BaseViewHolder
. It will be abstract, since all our classes will inherit from it ViewHolder
. It’s quite possible to do without it, but I still recommend making sure that all inheriting classes are the same in structure. (in Kotlin, extends
and implement
is used instead :
. As in Java, here you can inherit from one class and implement many interfaces) Next, we create our data classes: ItemTitle
, ItemGoogle
and ItemApple
. In Java, these are ordinary classes with a constructor and getters.
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
Let's focus on the interface here ListMarker
.
//это наш маркер. Его должны реализовать все айтемы, которые будут
//отображаться в конечном итоге в списке
interface ListMarker
All our data classes implement this interface to cast them to the same type. We will need this later for our list. Next, we create our viewholders for each element of the list. Don't forget that each of them inherits from BaseViewHolder
. In these classes we specify which element view
corresponds to the field from the data classes.
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()
}
}
There is no particular logic here, everything is extremely simple. Also, each viewholder has its own layout: list_item_title.xml
list_item_google.xml
list_item_apple.xml
Now let’s move on to the most difficult part - 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
//How вариант можно было сделать 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<*>
When we inherit our class from RecyclerView.Adapter
, we will need to override three methods: onCreateViewHolder
, getItemCount
, onBindViewHolder
. onCreateViewHolder
- here we initialize our classes ViewHolder
. getItemCount
— this method is responsible for the list size. onBindViewHolder
- here we pass the list elements to our classes ViewHolder
. For a regular list with one element type, this would be enough. But for different types we still need to redefine the method getItemViewType
(for this we use the constants that are at the top of our adapter class. In Java you can use final
variables for this). Also in Java, onBindViewHolder
instead of an expression, when
you can use a regular if
. And finally, let's move on to our main class - 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
This is where our and is initialized ListAdapter
. This is a pretty boilerplate code that many have encountered. In the method, stubData
I filled the list with data (as you can see, data with different elements) and passed this list to the adapter. Next, we launch our application, and we should see something like this on our screen: As you can see, there are different elements in one list, which was our goal. PS Forgot to mention the Extension
. This is what it looks like:
//это расширение для класса ViewGroup. Теперь мы можем по всему проекту использовать
//короткое inflate instead of длинного LayoutInflater.from(context).inflate
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
This is not a class, but a file, which is why there is no name inside it. But now we can use it in the adapter simply inflate
instead of a long structure. Unfortunately, this is not provided for Java, so write LayoutInflater.from(context).inflate
. That's all. Until next time :) Link to GitHub
GO TO FULL VERSION