I'm struggling to create a 'takeUntilSignal' operator for a Flow - an extension method that will cancel a flow when another flow generates an output.
fun <T> Flow<T>.takeUntilSignal(signal: Flow<Unit>): Flow<T>
My initial effort was to try to launch collection of the signal flow in the same coroutine scope as the primary flow collection, and cancel the coroutine scope:
fun <T> Flow<T>.takeUntilSignal(signal: Flow<Unit>): Flow<T> = flow {
kotlinx.coroutines.withContext(coroutineContext) {
launch {
signal.take(1).collect()
println("signalled")
cancel()
}
collect {
emit(it)
}
}
}
But this isn't working (and uses the forbidden "withContext" method that is expressly stubbed out by Flow to prevent usage).
edit
I've kludged together the following abomination, which doesn't quite fit the definition (resulting flow will only cancel after first emission from primary flow), and I get the feeling there's a far better way out there:
fun <T> Flow<T>.takeUntilSignal(signal: Flow<Unit>): Flow<T> =
combine(
signal.map { it as Any? }.onStart { emit(null) }
) { x, y -> x to y }
.takeWhile { it.second == null }
.map { it.first }
edit2
another try, using channelFlow:
fun <T> Flow<T>.takeUntilSignal(signal: Flow<Unit>): Flow<T> =
channelFlow {
launch {
signal.take(1).collect()
println("hello!")
close()
}
collect { send(it) }
close()
}
Use couroutineScope and start the new coroutine inside:
fun <T> Flow<T>.takeUntilSignal(signal: Flow<Unit>): Flow<T> = flow {
try {
coroutineScope {
launch {
signal.take(1).collect()
println("signalled")
this#coroutineScope.cancel()
}
collect {
emit(it)
}
}
} catch (e: CancellationException) {
//ignore
}
}
Check it https://github.com/hoc081098/FlowExt
package com.hoc081098.flowext
import com.hoc081098.flowext.internal.ClosedException
import com.hoc081098.flowext.internal.checkOwnership
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
/**
* Emits the values emitted by the source [Flow] until a [notifier] [Flow] emits a value or completes.
*
* #param notifier The [Flow] whose first emitted value or complete event
* will cause the output [Flow] of [takeUntil] to stop emitting values from the source [Flow].
*/
public fun <T, R> Flow<T>.takeUntil(notifier: Flow<R>): Flow<T> = flow {
try {
coroutineScope {
val job = launch(start = CoroutineStart.UNDISPATCHED) {
notifier.take(1).collect()
throw ClosedException(this#flow)
}
collect { emit(it) }
job.cancel()
}
} catch (e: ClosedException) {
e.checkOwnership(this#flow)
}
}
Related
I have my code below
interface Listener {
fun onGetData(data: Int)
fun onClose()
}
class MyEmitter {
var listener: Listener? = null
fun sendData(data: Int) = listener?.onGetData(data)
fun close() = listener?.onClose()
}
fun handleInput(myEmitter: MyEmitter) = channelFlow {
myEmitter.listener = object:Listener {
override fun onGetData(data: Int) { trySend(data) }
override fun onClose() { close() }
}
}
fun main(): Unit = runBlocking {
val myEmitter = MyEmitter()
handleInput(myEmitter).collect {
println(it)
}
myEmitter.sendData(1)
myEmitter.sendData(2)
myEmitter.close()
}
Whenever I send the data e.g. myEmitter.sendData(1), it does get into trySend(data), but the result is closed.
Why is it closed? How can I keep it open?
I think it's not documented terribly clearly, but just like the flow builder, the channelFlow's Flow is considered complete once the suspend lambda returns. Since all you are doing is setting a listener and not waiting around, it will return almost immediately. When a channel Flow is completed, it's channel is also closed.
If you want your channelFlow to stay open until the Flow is canceled, call awaitClose() at the end. This function suspends until the channel is closed, so it will hold your Flow open until it's canceled or the event in your listener closes the Channel.
fun handleInput(myEmitter: MyEmitter) = channelFlow {
myEmitter.listener = object:Listener {
override fun onGetData(data: Int) { trySend(data) }
override fun onClose() { close() }
}
awaitClose()
}
If you are familiar with callbackFlow, it is a specialized version of channelFlow and it enforces the awaitClose() call because it is meant for waiting for a listener, so there's no reason you would ever not want to await. It's also where you can deregister any listener you created inside the flow builder.
To get this working, I did 3 things
Add awaitClose to ensure the flow is not terminated
Move the entire flow behind launch, so that it is not blocking the main() function flow
Add a little delay before myEmitter.sendData(1), so that to ensure the launch get triggered first before doing the external sendData.
Full changed code as below
interface Listener {
fun onGetData(data: Int)
fun onClose()
}
class MyEmitter {
var listener: Listener? = null
fun sendData(data: Int) = listener?.onGetData(data)
fun close() = listener?.onClose()
}
fun handleInput(myEmitter: MyEmitter) = channelFlow {
myEmitter.listener = object:Listener {
override fun onGetData(data: Int) { trySend(data) }
override fun onClose() { close() }
}
awaitClose { myEmitter.listener = null } // Need awaitClose to keep the flow alive
}
fun main(): Unit = runBlocking {
val myEmitter = MyEmitter()
launch { // Need to run to avoid it from blocking the main() function flow due to having `awaitClose` there
handleInput(myEmitter).collect {
println(it)
}
}
delay(100) // Add some delay to get this triggered after the launch run
myEmitter.sendData(1)
myEmitter.sendData(2)
myEmitter.close()
}
The 3rd step is a little hack I think.
I want to collect specific amount of values from Flow until value emitting timeout happened. Unfortunately, there are no such operators, so I've tried to implement my own using debounce operator.
The first problem is that producer is too fast and some packages are skipped and not collected at all (they are in onEach of original packages flow, but not in onEach of second flow of merge in withNullOnTimeout).
The second problem - after taking last value according to amount argument orginal flow is closed, but timeout flow still alive and finally produce timeout after last value.
How can I solve this two problems?
My implementations:
suspend fun receive(packages: Flow<ByteArray>, amount: Int): ByteArray {
val buffer = ByteArrayOutputStream(blockSize.toInt())
packages
.take(10)
.takeUntilTimeout(100) // <-- custom timeout operator
.collect { pck ->
buffer.write(pck.data)
}
return buffer.toByteArray()
}
fun <T> Flow<T>.takeUntilTimeout(durationMillis: Long): Flow<T> {
require(durationMillis > 0) { "Duration should be greater than 0" }
return withNullOnTimeout(durationMillis)
.takeWhile { it != null }
.mapNotNull { it }
}
fun <T> Flow<T>.withNullOnTimeout(durationMillis: Long): Flow<T?> {
require(durationMillis > 0) { "Duration should be greater than 0" }
return merge(
this,
map<T, T?> { null }
.onStart { emit(null) }
.debounce(durationMillis)
)
}
This was what initially seemed obvious to me, but as Joffrey points out in the comments, it can cause an unnecessary delay before collection terminates. I'll leave this as an example of a suboptimal, oversimplified solution.
fun <T> Flow<T>.takeUntilTimeout(durationMillis: Long): Flow<T> = flow {
val endTime = System.currentTimeMillis() + durationMillis
takeWhile { System.currentTimeMillis() >= endTime }
.collect { emit(it) }
}
Here's an alternate idea I didn't test.
#Suppress("UNCHECKED_CAST")
fun <T> Flow<T>.takeUntilTimeout(durationMillis: Long): Flow<T> {
val signal = Any()
return merge(flow { delay(durationMillis); emit(signal) })
.takeWhile { it !== signal } as Flow<T>
}
How about:
fun <T> Flow<T>.takeUntilTimeout(timeoutMillis: Long) = channelFlow {
val collector = launch {
collect {
send(it)
}
close()
}
delay(timeoutMillis)
collector.cancel()
close()
}
Using a channelFlow allows you to spawn a second coroutine so you can count the time independently, and quite simply.
I am trying to create a polling mechanism with kotlin coroutines using sharedFlow and want to stop when there are no subscribers and active when there is at least one subscriber. My question is, is sharedFlow the right choice in this scenario or should I use channel. I tried using channelFlow but I am unaware how to close the channel (not cancel the job) outside the block body. Can someone help? Here's the snippet.
fun poll(id: String) = channelFlow {
while (!isClosedForSend) {
try {
send(repository.getDetails(id))
delay(MIN_REFRESH_TIME_MS)
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
invokeOnClose { Timber.e("channel flow closed.") }
}
}
You can use SharedFlow which emits values in a broadcast fashion (won't emit new value until the previous one is consumed by all the collectors).
val sharedFlow = MutableSharedFlow<String>()
val scope = CoroutineScope(Job() + Dispatchers.IO)
var producer: Job()
scope.launch {
val producer = launch() {
sharedFlow.emit(...)
}
sharedFlow.subscriptionCount
.map {count -> count > 0}
.distinctUntilChanged()
.collect { isActive -> if (isActive) stopProducing() else startProducing()
}
fun CoroutineScope.startProducing() {
producer = launch() {
sharedFlow.emit(...)
}
}
fun stopProducing() {
producer.cancel()
}
First of all, when you call channelFlow(block), there is no need to close the channel manually. The channel will be closed automatically after the execution of block is done.
I think the "produce" coroutine builder function may be what you need. But unfortunately, it's still an experimental api.
fun poll(id: String) = someScope.produce {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val channel = poll("hello")
channel.receive()
channel.cancel()
}
The produce function will suspended when you don't call the returned channel's receive() method, so there is no need to delay.
UPDATE: Use broadcast for sharing values across multiple ReceiveChannel.
fun poll(id: String) = someScope.broadcast {
invokeOnClose { Timber.e("channel flow closed.") }
while (true) {
try {
send(repository.getDetails(id))
// delay(MIN_REFRESH_TIME_MS) //no need
} catch (throwable: Throwable) {
Timber.e("error -> ${throwable.message}")
}
}
}
fun main() = runBlocking {
val broadcast = poll("hello")
val channel1 = broadcast.openSubscription()
val channel2 = broadcast.openSubscription()
channel1.receive()
channel2.receive()
broadcast.cancel()
}
Given 2 or more flows with the same type, is there an existing Kotlin coroutine function to merge them, like the RX merge operator?
Currently I was considering this:
fun <T> merge(vararg flows: Flow<T>): Flow<T> = channelFlow {
val flowJobs = flows.map { flow ->
GlobalScope.launch { flow.collect { send(it) } }
}
flowJobs.joinAll()
}
but it seems somewhat clumsy.
This is now (Coroutines Version 1.3.5 at time of writing) part of the Coroutines library.
You use it like this:
val flowA = flow { emit(1) }
val flowB = flow { emit(2) }
merge(flowA, flowB).collect{ println(it) } // Prints two integers
// or:
listOf(flowA, flowB).merge().collect { println(it) } // Prints two integers
You can read more in the source code
I'm not too familiar with flows yet, so this might be suboptimal. Anyway, I think you could create a flow of all your input flows, and then use flattenMerge to flatten them into a single flow again. Something like this:
fun <T> merge(vararg flows: Flow<T>): Flow<T> = flowOf(*flows).flattenMerge()
Edit:
The merge-function was added to kotlinx-coroutines in the 1.3.3 release. See here: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/merge.html
It may be late but I believe this may be a viable solution:
fun <T> combineMerge(vararg flows: Flow<T>) = flow {
coroutineScope {
flows.forEach {
launch {
it.collect {
emit(it)
}
}
}
}
}
fun <T> combineConcat(vararg flows: Flow<T>) = flow {
flows.forEach {
it.collect {
emit(it)
}
}
}
Consider an asynchronous API that reports progress on its operations:
suspend fun operationWithIO(input: String, progressUpdate: (String) -> Unit): String {
withContext(Dispatchers.IO) {
// ...
}
}
Is it possible to implement calls to progressUpdate such that callbacks are handled on the caller's dispatcher? Or is there a better way to deliver status updates back to the caller?
You should send progress updates on a channel. That will allow the caller to listen to the channel using whatever dispatcher it wants.
suspend fun operationWithIO(input: String, progressChannel: Channel<String>): String {
withContext(Dispatchers.IO) {
// ...
progressChannel.send("Done!")
progressChannel.close()
}
}
The caller can use it by doing something like this:
val progressChannel = Channel<String>()
someScope.launch {
operationWithIO(input, progressChannel)
}
// Remember the call to progressChannel.close(), so that this iteration stops.
for (progressUpdate in progressChannel) {
println(progressUpdate)
}
How about wrapping the callback function and calling the wrapped function:
/** Return a new callback that invokes this callback on the current context. */
suspend fun <T> ((T) -> Unit).onCurrentContext(): (T) -> Unit =
coroutineContext.let { context ->
{ value: T ->
runBlocking {
launch(context) {
this#onCurrentContext.invoke(value)
}
}
}
}
/** Perform a background operation, delivering status updates on the caller's context. */
suspend fun operationWithIO(statusUpdate: (String) -> Unit): String {
val cb = statusUpdate.onCurrentContext()
return withContext(Dispatchers.IO) {
cb("Phase 1")
delay(150)
cb("Phase 2")
delay(150)
"Result"
}
}
// In use
runBlocking {
val result = operationWithIO {
println("received callback status $it")
}
println("result is $result")
}