Search code examples
kotlinenumsrx-javarx-java2

Using RxJava to generate a map where keys are values of a Kotlin enum and map's values come from another RxJava stream


Introduction

Let's say I have a Kotlin enum class:

enum class Type {
    ONE,
    TWO,
    THREE,
    FOUR
}

and a following data class:

data class Item(
    val name: String,
    val type: Type
)

Then I have a Single that emits a list of Items – can by anything but for example purposes, let's say it looks like that:

val itemsSingle = Single.just(listOf(
    Item("A", Type.ONE),
    Item("B", Type.ONE),
    Item("C", Type.TWO),
    Item("D", Type.THREE),
))

Problem

What I'd like to achieve is to have an RxJava stream that will output a map where keys come from Type and values are lists of Items matching a given Type value (where an undetermined, unsorted list of Items is provided by some other Single stream). The signature would be:

Single<Map<Type, List<Item>> // or Observable<Map<Type, List<Item>>

One additional requirement is that the map's keys should always exhaust all values from Type enum even if the itemsSingle stream contains no items for some Type values (or no items at all). So, for the provided itemsSingle example stream the returned map should look like this:

{
    ONE:   [ Item(name: "A", type: ONE), Item(name: "B", type: ONE) ],
    TWO:   [ Item(name: "C", type: TWO) ],
    THREE: [ Item(name: "D", type: THREE) ],
    FOUR:  []
}

Attempt

With all the above, I've kinda achieved the desired result with following steps:

  1. To satisfy the requirement of exhausting all Type enum values I first create a map that has an empty list for all possible Type values:
val typeValuesMap = Type.values().associate { it to emptyList<Item>() }
val typeValuesMapSingle = Single.just(typeValuesMap)

// result: {ONE=[], TWO=[], THREE=[], FOUR=[]}
  1. I can get a map that contains items from itemsSingle grouped under respective Type value keys:
val groupedItemsMapSingle = itemsSingle.flattenAsObservable { it }
    .groupBy { it.type }
    .flatMapSingle { it.toList() }
    .toMap { list -> list[0].type } // the list is guaranteed to have at least one item

// result: {ONE=[Item(name=A, type=ONE), Item(name=B, type=ONE)], THREE=[Item(name=D, type=THREE)], TWO=[Item(name=C, type=TWO)]}
  1. finally I can combine both lists using the combineLatest operator and overwriting initial empty list of items for a given Type value if itemsSingle contained any Items for this Type value:
Observable.combineLatest(
    typeValuesMapSingle.flattenAsObservable { it.entries },
    groupedItemsMapSingle.flattenAsObservable { it.entries }
) { a, b -> listOf(a, b) }
    .defaultIfEmpty(typeValuesMap.entries.toList()) // in case itemsSingle is empty
    .flatMapIterable { it }
    .collect({mutableMapOf<Type, List<Item>>()}, { a, b -> a[b.key] = b.value})

// result: {FOUR=[], ONE=[Item(name=A, type=ONE), Item(name=B, type=ONE)], THREE=[Item(name=D, type=THREE)], TWO=[Item(name=C, type=TWO)]}

Summary

As you can see, it's quite a lot of code for a seemingly simple operation. So my question is – is there a simpler way to achieve the result I'm after?


Solution

  • Just merge a map of empty lists with a map of filled lists

    val result = itemsSingle.map { items->
       Type.values().associateWith { listOf<Item>() } + items.groupBy { it.type }
    }