Is it good practice to call `launch` here even tho...
# coroutines
r
Is it good practice to call
launch
here even though I'm inside a suspending function?
Copy code
private val scope: CoroutineScope = ..
private val connectionState = MutableStateFlow<Connection?>(null)

suspend fun connect(): Connection = withContext(scope.coroutineContext) {
    val sharedFlow: SharedFlow<Connection> = connectionFlow()
        .shareIn(this, SharingStarted.Eagerly, replay = 1)
    launch { sharedFlow.collect(connectionState) }
    sharedFlow.first()
}
s
Depends 🙂 First, to not break the structured concurrency Use
coroutineScope { ..... launch { ..... }. }
inside your
connect
function instead, making sure that the
launch
is part of the same coroutine-scope that called your
connect
function. when using structured concurrency, note that the call to your
connect
function only returns(\resumes) after all the child coroutines (ie your launch) have returned/completed! That means that the
sharedFlow.collect
must have completed/returned. And that the call to
sharedFlow.first()
must have completed/returned.
But calling
coroutineScope { .... launch { ....} or .... val result = async { ...}
is perfectly already if you want to do some parallel/concurrent work within your suspend function (
collect
) But as i noted, the
collect
will only return/resume after all these parallel/concurrent tasks have completed as well due to the structured concurrency of coroutines.
r
Glad I asked then, I originally wrote this using
async
and ran into the issue where my function wasn't returning due to the `launch`/`collect` still running so I switched to
withContext
. I missed the
coroutineScope
though, so I should be having the same "problem". Thanks! Do you have a suggestion on how I could redesign this so that I can suspend for the first value but collect on the shared flow even after the function returns?
It feels wrong to do
Copy code
suspend fun connect(): Connection {
  val flow = ..
  scope.launch { .. }
  return flow.first()
}
s
Copy code
suspend fun connect(): Connection = coroutineScope {
    val sharedFlow: SharedFlow<Connection> = connectionFlow()
        .shareIn(this, SharingStarted.Eagerly, replay = 1)
    launch { sharedFlow.collect(connectionState) }
    sharedFlow.first()
}
@reline Yup, it looks a bit off .... 🙂 But for answering the original question, about doing async/launch within suspend fun; that is certainly possible.
Also IIRC,
first
is a terminating function... no need to call 'collect' as well, even asynchronously
connectionFlow().shareIn(someScope, haringStarted.Eagerly, replay = 1).first()
should do it.
r
right, but I'd like subsequent emissions from my connection flow to emit to my state flow
it's essentially a lazy collection, normally I would launch a coroutine in the
init
block and collect on the state flow, then just query the state flow
Copy code
init {
  scope.launch { connectionFlow().collect(stateFlow) }
}

fun connect(): Connection = stateFlow.filterNotNull().first()
but I don't want to begin collecting the flow until
connect()
is called
I'm picturing using an actor pattern but that seems overly complex
Copy code
private val actor = scope.watchState()

fun CoroutineScope.watchState() = actor {
  for (item in channel) {
    // connectionFlow is a channelFlow which closes upon disconnect
    connectionFlow().collect(stateFlow)
  }
}

suspend fun connect(): Connection {
  return stateFlow.value ?: run {
    actor.send(Unit) // could also pass a `CompletableDeferred` and `await()`
    stateFlow.filterNotNull().first()
  }
}