android

Sectioned RecyclerView in three steps

Hey! Today I want to share with you another quick tip that often comes when we deal with RecyclerView – sections. For that task I will use the power of Kotlin language – sealed classes and transformations on collections!

Let’s say you have an API that returns you list of groceries:

data class Food(val name: String, val category: String)

val groceries = listOf(
            Food("orange", "fruit"),
            Food("juice", "drink"),
            Food("potato", "vegetable"),
            Food("banana", "fruit"),
            Food("water", "drink"),
            Food("apple", "fruit"),
            Food("cucumber", "vegetable"),
            Food("salad", "vegetable")
)

What you want to do, is to display the groceries based on the category, whether it’s fruit, vegetable, drink etc. with corresponding sections headers above each category. With Kotlin it’s super easy.

First step

We just need to create sealed class with two classes: Food and Section. Those classes are going to be used by RecyclerView to display proper view – either Section or Food.

sealed class RecyclerItem {
        data class Food(val name: String): RecyclerItem()
        data class Section(val title: String): RecyclerItem()
}

Second step

When we have our list of groceries, we need to transform it to sectioned list that is grouped based on the food category:

val sectionedGroceries: List<RecyclerItem> = groceries
                .groupBy { it.category }
                .flatMap { (category, foods) ->
                    listOf<RecyclerItem>(RecyclerItem.Section(category)) + foods.map { RecyclerItem.Food(it.name) }
                }

Third step

What we want to do now is we just need to display list in RecyclerView divided into proper sections. That’s why we created sealed class – look how easily we can do that:

// super handy extension function from this post: https://antonioleiva.com/kotlin-awesome-tricks-for-android/
fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
    return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
}

class GroceriesAdapter(val sectionedGroceries: List<RecyclerItem>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    private val TYPE_SECTION = 0
    private val TYPE_FOOD = 1
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when(viewType) {
        TYPE_SECTION -> SectionViewHolder(parent.inflate(R.layout.layout_section))
        TYPE_FOOD -> FoodViewHolder(parent.inflate(R.layout.layout_food))
        else -> ErrorViewHolder(parent.inflate(R.layout.layout_error))
    }

    override fun getItemCount() = sectionedGroceries.size
    
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        when(val item = sectionedGroceries[holder.adapterPosition]) {
            is RecyclerItem.Food -> (holder as FoodViewHolder).bind(item)
            is RecyclerItem.Section -> (holder as SectionViewHolder).bind(item)
        }
    }

    override fun getItemViewType(position: Int) = when(sectionedGroceries[position]) {
        is RecyclerItem.Food -> TYPE_FOOD
        is RecyclerItem.Section -> TYPE_SECTION
    }
}

And that’s it! With Kotlin’s syntactic sugar we can make our code really simple and powerful with few lines of code. Stay tuned!

Mariusz Brona
4 KOMENTARZE
  • Frank
    Odpowiedz

    `val item = sectionedGroceries[holder.adapterPosition]` will return RecyclerItem, so to get food value and section value how to do that?

    1. Mariusz Brona
      Odpowiedz

      It is described in third step – using `is` Kotlin keyword you can check what is the subtype of RecyclerItem – Food or Section, and then use corresponding value by casting (item as RecyclerItem.Food).name or (item as RecyclerItem.Section).title 🙂

  • Kelvin
    Odpowiedz

    .flatMap { (category, foods) ->
    listOf(RecyclerItem.Section(category)) + foods.map { RecyclerItem.Food(it.name) }
    }

    Where does `category` and `foods` come from?
    They are not featured in any for the steps

    1. Mariusz Brona
      Odpowiedz

      When you are using groupBy operator, it returns this:

      Map>

      In our case, when we’re grouping the list of groceries by category, groupBy basically returns this (sorry for pseudocode):

      Map)

      Then in flatMap we are just using map parameter, however it is destructured to this notation for easier reading:

      flatMap { (category, foods) -> }

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *