androidkotlinandroid-recyclerviewlistadapter

list adapter with sections


I already have a list adapter that works properly. But I want to divide the object in the list into sections according to the date they were created. Something like this:

enter image description here

I found something called "sectioned recycler view" but couldn't find any documentation on that. I read all the related questions, but they all are either outdated or use a third-party library. What's the native way of implementing this feature?


Solution

  • There are a couple of approaches you could use. First the easy one:

    The logic there depends on what you want, but it could be as simple as

    val visible = position == 0 || items[position].date != items[position - 1].date
    

    Basically you just need to work out what the condition is that would cause an item to be in a different "group" than the previous item, and then if it's met, show the header over that item.


    The approach MarkL is talking about is more complex, but it's also more robust - by having separate Item and Header elements, you can treat them differently, and even do stuff like having the header collapse/show its children, select them all etc. You can do that with the other approach, but it requires more code since you're not really treating things as groups, it's more of a trick when it comes to displaying stuff.

    Basically, ignoring the how for now, you need a list of headers and items. A sealed class is a good way to represent that:

    sealed class ListElement {
        data class Header(val date: Date) : ListElement()
        data class Item(val itemData: YourItem) : ListElement()
    } 
    

    I've made Item a wrapper class that holds your actual data object inside, since that's probably coming from elsewhere and you can't define it as part of this sealed class hierarchy - so sticking it inside one of the subclasses like this allows you to do that.

    So now you can have a List<ListElement> containing Headers and Items in display order. Since you've mentioned creating the ViewHolders in a comment I won't explain all that, but basically when you're getting the item type for a position, you just need to check is Header or is Item and then handle it from there.


    As for creating that list, there are lots of ways to do it - you could use groupBy to generate a Map of dates to lists of items, map each of those entries to a list of Header, Item, Item..., and flatten the whole thing into a single list:

    items.map { Item(it) }
        .groupBy { it.itemData.date }
        .entries
        .flatMap { (date, items) -> listOf(Header(date)) + items }
    

    The advantage with a map like this is it's an actual grouped structure, so you can keep it around to generate flat lists for display - e.g. hiding a group's contents by only including the header in the list.

    Or you could just build the list yourself, similar to the logic I mentioned in the first example - if the date has changed from the previous item, insert a Header first:

    val list = mutableListOf<ListElement>().apply {
        for (item in items) {
            // add a header if the date changed - this handles the first header
            // in an empty list too (where lastOrNull returns null, so the date is null)
            val previousItemDate = (lastOrNull() as? Item)?.itemData?.date
            if (previousItemDate != item.date) add(Header(item.date))
            add(Item(item))
        }
    }
    

    Or you could use fold. Don't forget to sort stuff!