How to concat multiple Kotlin Flows sequencially? - kotlin

I'm looking for a way to subscribe to multiple Kotlin Flows sequencially, something similar to rxjs's concat operator. Next Flow should only be subscribed once previous one is completed.
Example:
val flow1 = flowOf(0,1,2).onEach { delay(10) }
val flow2 = flowOf(3,4,5).onEach { delay(10) }
runBlocking{
listOf(flow1,flow2)
.merge()
.onEach { println(it) }
.collect()
}
-> prints 0,3,1,4,2,5 // Flow order is not preserved
I imagine a solution could be to replace merge() with concat(), but this operator sadly doesn't exist in Kotlin Flows
val flow1 = flowOf(0,1,2).onEach { delay(10) }
val flow2 = flowOf(3,4,5).onEach { delay(10) }
runBlocking{
listOf(flow1,flow2)
.concat()
.onEach { println(it) }
.collect()
}
-> prints 0,1,2,3,4,5 // Flow order is now preserved

You're looking for flattenConcat.
It has a Flow<Flow<T>> receiver, but you could easily make a Flow out of your list of flows, for instance using list.asFlow(). Or you could create a Flow directly instead of a list by using flowOf instead of listOf.

Related

Flows in Kotlin not collecting all values

I'm doing some exercises to learn Flows in Kotlin, and I found some issues which I cannot understand.
When using a MutableStateFlow, in the next example it only prints the number 3. I would expect to print 0 to 3 instead. One could say that maybe is going too fast, or I should put a delay, but this seems to me a patch if such is the case, since if it is true that if sending MutableStateFlow data too fast makes it skip some values, then is something to consider every single time when using it.
val flow = MutableStateFlow<Int>(0)
fun main(): Unit = runBlocking {
launch {
flow.collect {
println(it)
}
}
(0..3).forEach {
flow.emit(it)
}
}
// Expected to print 0, 1, 2, 3
// Printing only 3
Next, I tried to use a MutableSharedFlow instead, but it emits nothing at all, not even 3. Same code as above but replacing the flow with:
val flow = MutableSharedFlow<Int>()
MutableStateFlow cannot be used here because its behavior does not allow to get every value
so I used SharedFlow
Example with SharedFlow:
val flow = MutableSharedFlow<Int>()
fun main(): Unit = runBlocking {
val scope = // scope
flow
.onEach {
println(it)
}
.launchIn(scope)
(0..3).forEach {
flow.emit(it)
}
// delay to wait for println
launch {
delay(10000)
}
}

Combine a Flow and a non Flow api response Kotlin

I currently have a piece of logic as follows:
interface anotherRepository {
fun getThings(): Flow<List<String>>
}
interface repository {
suspend fun getSomeThings(): AsyncResult<SomeThings>
}
when (val result = repository.getSomeThings()) {
is AsyncResult.Success -> {
anotherRepository.getThings().collectLatest {
// update the state
}
else -> { }
}
}
The problem I am having is that, if repository.getSomeThings has been triggered multiple times before, anotherRepository.getThings is getting triggered for the amount of all the pre-loaded values from repository.getSomeThings. I was wondering what is the proper way to use these repositories, one a suspend function, the other a Flow together. The equivalent behaviour that is combineLatest{} in Rx.
Thank you.
There are a couple of ways to solve your problem. One way is just to call
repository.getSomeThings() in the collectLatest block and cache last result:
var lastResult: AsyncResult<SomeThings>? = null
anotherRepository.getThings().collectLatest {
if (lastResult == null) {
lastResult = repository.getSomeThings()
}
// use lastResult and List<String>
}
Another approach is to create a Flow, which will be calling repository.getSomeThings() function, and combine two Flows:
combine(
anotherRepository.getThings(),
flow {emit(repository.getSomeThings())}
) { result1: List<String>, result2: AsyncResult<SomeThings> ->
...
}

Kotlin Kafka consumer

I am new to kotlin and kafka. I am trying to read message from a kafka consumer and then process it later in kotlin.
I am able to print the message consumed from kafka topic.
However, I am not able to add this message into a list and process it later.
How to add the message from kafka topic to a list to be processed later?
Below is my kafka consumer's consume method.
KafkaConsumer.kt
fun consume(handler: (value: String) -> Unit) = thread(start = true) {
keepGoing = true
consumer.use { consumer ->
while (keepGoing) {
consumer.poll(Duration.ofMillis(500))?.forEach {
println(it?.value() ?: "no value")
handler(it?.value() ?: "empty_message")
}
}
}
}
I am invoking the consume() as shown below
fun main(){
<read config>
val kafkaConsumer = KafkaConsumer(config.get(<topicname>))
kafkaConsumer.consume {
println(it)
}
}
PS: I tried to append the to a mutable list in the main() which did not work.
I'm not sure if it's a formatting issue or your actual code, but Kotlin lambdas are defined by { } and you call consume() with 2 pairs of braces:
kafkaConsumer.consume { it -> {
println(it)
} }
This is effectively passing a function that takes one argument and returns a function. That returned function takes itself an argument and prints it.
This should not even compile given the signature of consume().
What you probably want is this instead:
kafkaConsumer.consume { it ->
println(it)
}
Note that it is also the implicit argument of lambda expressions, so you don't even need to specify it at all:
kafkaConsumer.consume {
println(it)
}

Implement backoff strategy in flow

I'm trying to implement a backoff strategy just using kotlin flow.
I need to fetch data from timeA to timeB
result = dataBetween(timeA - timeB)
if the result is empty then I want to increase the end time window using exponential backoff
result = dataBetween(timeA - timeB + exponentialBackOffInDays)
I was following this article which is explaining how to approach this in rxjava2.
But got stuck at a point where flow does not have takeUntil operator yet.
You can see my implementation below.
fun main() {
runBlocking {
(0..8).asFlow()
.flatMapConcat { input ->
// To simulate a data source which fetches data based on a time-window start-date to end-date
// available with in that time frame.
flow {
println("Input: $input")
if (input < 5) {
emit(emptyList<String>())
} else { // After emitting this once the flow should complete
emit(listOf("Available"))
}
}.retryWhenThrow(DummyException(), predicate = {
it.isNotEmpty()
})
}.collect {
//println(it)
}
}
}
class DummyException : Exception("Collected size is empty")
private inline fun <T> Flow<T>.retryWhenThrow(
throwable: Throwable,
crossinline predicate: suspend (T) -> Boolean
): Flow<T> {
return flow {
collect { value ->
if (!predicate(value)) {
throw throwable // informing the upstream to keep emitting since the condition is met
}
println("Value: $value")
emit(value)
}
}.catch { e ->
if (e::class != throwable::class) throw e
}
}
It's working fine except even after the flow has a successful value the flow continue to collect till 8 from the upstream flow but ideally, it should have stopped when it reaches 5 itself.
Any help on how I should approach this would be helpful.
Maybe this does not match your exact setup but instead of calling collect, you might as well just use first{...} or firstOrNull{...}
This will automatically stop the upstream flows after an element has been found.
For example:
flowOf(0,0,3,10)
.flatMapConcat {
println("creating list with $it elements")
flow {
val listWithElementCount = MutableList(it){ "" } // just a list of n empty strings
emit(listWithElementCount)
}
}.first { it.isNotEmpty() }
On a side note, your problem sounds like a regular suspend function would be a better fit.
Something like
suspend fun getFirstNonEmptyList(initialFrom: Long, initialTo: Long): List<Any> {
var from = initialFrom
var to = initialTo
while (coroutineContext.isActive) {
val elements = getElementsInRange(from, to) // your "dataBetween"
if (elements.isNotEmpty()) return elements
val (newFrom, newTo) = nextBackoff(from, to)
from = newFrom
to = newTo
}
throw CancellationException()
}

Equivalent of RxJava .toList() in Kotlin coroutines flow

I have a situation where I need to observe userIds then use those userIds to observe users. Either userIds or users could change at any time and I want to keep the emitted users up to date.
Here is an example of the sources of data I have:
data class User(val name: String)
fun observeBestUserIds(): Flow<List<String>> {
return flow {
emit(listOf("abc", "def"))
delay(500)
emit(listOf("123", "234"))
}
}
fun observeUserForId(userId: String): Flow<User> {
return flow {
emit(User("${userId}_name"))
delay(2000)
emit(User("${userId}_name_updated"))
}
}
In this scenario I want the emissions to be:
[User(abc_name), User(def_name)], then
[User(123_name), User(234_name)], then
[User(123_name_updated), User(234_name_updated)]
I think I can achieve this in RxJava like this:
observeBestUserIds.concatMapSingle { ids ->
Observable.fromIterable(ids)
.concatMap { id ->
observeUserForId(id)
}
.toList()
}
What function would I write to make a flow that emits that?
I believe you're looking for combine, which gives you an array that you can easily call toList() on:
observeBestUserIds().collectLatest { ids ->
combine(
ids.map { id -> observeUserForId(id) }
) {
it.toList()
}.collect {
println(it)
}
}
And here's the inner part with more explicit parameter names since you can't see the IDE's type hinting on Stack Overflow:
combine(
ids.map { id -> observeUserForId(id) }
) { arrayOfUsers: Array<User> ->
arrayOfUsers.toList()
}.collect { listOfUsers: List<User> ->
println(listOfUsers)
}
Output:
[User(name=abc_name), User(name=def_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]
Live demo (note that in the demo, all the output appears at once, but this is a limitation of the demo site - the lines appear with the timing you'd expect when the code is run locally)
This avoids the (abc_name_updated, def_name_updated) discussed in the original question. However, there's still an intermediate emission with 123_name_updated and 234_name because the 123_name_updated is emitted first and it sends the combined version immediately because they're the latest from each flow.
However, this can be avoided by debouncing the emissions (on my machine, a timeout as small as 1ms works, but I did 20ms to be conservative):
observeBestUserIds().collectLatest { ids ->
combine(
ids.map { id -> observeUserForId(id) }
) {
it.toList()
}.debounce(timeoutMillis = 20).collect {
println(it)
}
}
which gets you the exact output you wanted:
[User(name=abc_name), User(name=def_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]
Live demo
This is unfortunatly non trivial with the current state of kotlin Flow, there seem to be important operators missing. But please notice that you are not looking for rxJavas toList(). If you would try to to do it with toList and concatMap in rxjava you would have to wait till all observabes finish.
This is not what you want.
Unfortunately for you I think there is no way around a custom function.
It would have to aggregate all the results returned by observeUserForId for all the ids which you would pass to it. It would also not be a simple windowing function, since in reality it is conceivable that one observeUserForId already returned twice and another call still didn't finish. So checking whether you already have the same number of users as you passed ids into your aggregating functions isn't enought, you also have to group by user id.
I'll try to add code later today.
Edit: As promised here is my solution I took the liberty of augmenting the requirements slightly. So the flow will emit every time all userIds have values and an underlying user changes. I think this is more likely what you want since users probably don't change properties in lockstep.
Nevertheless if this is not what you want leave a comment.
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.runBlocking
data class User(val name: String)
fun observeBestUserIds(): Flow<List<String>> {
return flow {
emit(listOf("abc", "def"))
delay(500)
emit(listOf("123", "234"))
}
}
fun observeUserForId(userId: String): Flow<User> {
return flow {
emit(User("${userId}_name"))
delay(2000)
emit(User("${userId}_name_updated"))
}
}
inline fun <reified K, V> buildMap(keys: Set<K>, crossinline valueFunc: (K) -> Flow<V>): Flow<Map<K, V>> = flow {
val keysSize = keys.size
val valuesMap = HashMap<K, V>(keys.size)
flowOf(*keys.toTypedArray())
.flatMapMerge { key -> valueFunc(key).map {v -> Pair(key, v)} }
.collect { (key, value) ->
valuesMap[key] = value
if (valuesMap.keys.size == keysSize) {
emit(valuesMap.toMap())
}
}
}
fun observeUsersForIds(): Flow<List<User>> {
return observeBestUserIds().flatMapLatest { ids -> buildMap(ids.toSet(), ::observeUserForId as (String) -> Flow<User>) }
.map { m -> m.values.toList() }
}
fun main() = runBlocking {
observeUsersForIds()
.collect { user ->
println(user)
}
}
This will return
[User(name=def_name), User(name=abc_name)]
[User(name=123_name), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name)]
[User(name=123_name_updated), User(name=234_name_updated)]
You can run the code online here
You can use flatMapConcat
val users = observeBestUserIds()
.flatMapConcat { ids ->
flowOf(*ids.toTypedArray())
.map { id ->
observeUserForId(id)
}
}
.flattenConcat()
.toList()
or
observeBestUserIds()
.flatMapConcat { ids ->
flowOf(*ids.toTypedArray())
.map { id ->
observeUserForId(id)
}
}
.flattenConcat()
.collect { user ->
}