Home > Net >  Kotlin list transform data stream
Kotlin list transform data stream

Time:11-27

I'm building a chat platform, where I'm reading my list of messages from Local Room DB (ChatModel). I need to add date separators between these messages. I've to use multiple view holders and thus created a sealed class for differentiating items

sealed class ChatUiModel {
    data class ChatItem(val message: ChatModel) : ChatUiModel()
    data class DateSeparatorItem(val time: String) : ChatUiModel()
}

I require to convert the list with date separate items in between 2 models of the list, I'm not proficient with Collection functions in kotlin and confused between map/flatmap etc.

.observe(viewLifecycleOwner) { messages ->
                            messages.map {
                                // if item prev.date < item next.date
                  ChatUiModel.DateSeparatorItem(it.date.toReadableTime())
                                ChatUiModel.ChatItem(it)
                            }
                            chatAdapter.submitList(messages)
                        }

Reached to this

val items = mutableListOf<ChatUiModel>()
                            val data = messages.listIterator()
                            for (item in data) {
                                if (data.hasPrevious())
                                    if (data.previous().time < item.time)
                                        items.add(ChatUiModel.DateSeparatorItem(item.time))
                                items.add(ChatUiModel.ChatItem(item))
                            }
                            Timber.i("CHAT = $items")

CodePudding user response:

(this ended up long but I thought you'd like an explanation of what's going on - you can just skip to the end for the solutions if you want)

Ok, so this is a little tricky if you're not familiar with all the utility functions and general functional manipulation - what you're basically doing is transforming incoming messages into ChatItems, but you also want to compare each message to the previous one, and output a DateSeparatorItem first where necessary, right?

A straight map isn't going to work - that just transforms each item into another item (it's mapping one value to another), and sometimes you want to transform one item into two (a date item and a chat item).

You could map each message item into a list, and make that contain either a chat item, or a date chat. So that would give you a list of lists, which you could then flatten so you just get all those items in order, in a single list. That's basically what flatmap does!


So now you need to be able to compare multiple messages, so you can check the dates. Kotlin has this windowed function that acts like a sliding view across your collection, so it can transform [1, 2, 3, 4] into [[1, 2], [2, 3], [3, 4]], and then you can work on those groups. There's a more convenient zipWithNext function that only produces Pairs instead of arbitrarily sized Lists - i.e. [(1, 2), (2, 3), (3, 4)], but windowed has a useful option - partialWindows allows that window to keep moving to the end of the list, even as it runs out of items to fill the full window:

listOf(1, 2, 3, 4).windowed(size=3, partialWindows=true).run(::println)

>> [[1, 2, 3], [2, 3, 4], [3, 4], [4]]

If we do this for a window of size 2, we get every original message, and also the one following it if there is one (zipWithNext will stop when it runs out of complete pairs):

listOf(1, 2, 3, 4).windowed(size=2, partialWindows=true).run(::println)

>> [[1, 2], [2, 3], [3, 4], [4]]

We can use this!


Your logic right now is taking a message and comparing it to the previous one to see if a date needs inserting before the chat item - I'd suggest flipping that around, and inserting a date after the current item by checking the next item's timestamp. That's because windowed is giving you each item along with the next one, so you don't get to look at the previous one.

We're working with a list here, and we need to compare the first item to the second one (checking if there even is one), but we can be a little bit cheeky and just compare list.first() with list.last(). We know there's gonna be either one or two items - and if there's only one item in the list (i.e. it's the last message) then we're comparing it with itself, and since we're only adding the date item if the timestamps are different... well they won't be if it's the same item! So there won't be any rogue date items added at the end. Probably worth documenting the code if you do that since it might not be clear - you can write some more explicit logic if you want.


Here's a few ways to do the final thing: Kotlin Playground example

data class Message(val text: String, val time: Int)

val messages = listOf(
    Message("hey", 1),
    Message("u up", 1),
    Message("lol", 3),
    Message("wow", 10)
)

fun withMutableList() {
    messages.windowed(size=2, partialWindows=true)
    // or map followed by flatten()
    .flatMap { items ->
        val current = items.first()
        val next = items.last()
        // creating a mutable list with the chat item, optionally adding a date
        mutableListOf<ChatUiModel>(ChatItem(current)).apply {
            if (next.time > current.time) add(DateItem(next.time))
        }
    }
    .forEach(::println)
}
fun withNulls() {
    messages.windowed(size=2, partialWindows=true)
    .flatMap { items ->
        val current = items.first()
        val next = items.last()
        // either adding a date or a null, nulls get removed later
        listOf(
            ChatItem(current),
            if (next.time > current.time) DateItem(next.time) else null
        )
    }
    .filterNotNull()
    .forEach(::println)
}
fun withSequence() {
    sequence {
        messages.windowed(size=2, partialWindows=true)
            .forEach { items ->
                val current = items.first()
                val next = items.last()
                // just yielding a stream of items, nice and neat!
                yield(ChatItem(current))
                if (next.time > current.time) yield(DateItem(next.time))
            }
    }.forEach(::println)
}

all giving this output:

ChatItem(message=Message(text=hey, time=1))
ChatItem(message=Message(text=u up, time=1))
DateItem(time=3)
ChatItem(message=Message(text=lol, time=3))
DateItem(time=10)
ChatItem(message=Message(text=wow, time=10))

CodePudding user response:

An easy way to prepare the list can be:

messages
    .groupBy { it.date }
    .map { (date, chatModels) ->
        listOf(DateSeparatorItem(date))   chatModels.map { ChatItem(it) }
    }
    .flatten()

try it yourself

Here we first group all the messages by their data to get a Map<Long, List<ChatModel>. Then we map each entry of the map to a new list containing the DateSeparator and the ChatItems for that date. Finally, we flatten the entire list to get the desired List<ChatUiModel>.

  • In the code that I linked, I have used Long for the date. If you have a String you can easily interconvert them using java.time APIs.
  • If your messages list is not sorted initially, add a sortedBy function before groupBy to sort it first.
  • Related