Hello, I’d like to invite some feedback on a desig...
# coroutines
c
Hello, I’d like to invite some feedback on a design I have. I am trying to model a stateful connection using a
StateFlow<Job>
, so that connection lifecycles can be observed, cancelled, reconnected, etc. I have an Android app which connects to several websocket endpoints on different protocols, including plain websockets, socketio, action cable, graphql subscriptions, so being able to define a common pattern like this would be useful to me. More in 🧵
I have the following test program:
Copy code
fun main(args: Array<String>) = runBlocking {
  println("Program start")
  val jobLoop = LazyJobLoop(Dispatchers.Default)
  jobLoop.job.collect {
    withContext(it) {
      println("In withContext")
    }
  }
  Unit
}

interface JobLoop : CoroutineScope {
  val job: StateFlow<Job>
}

class LazyJobLoop(
  coroutineContext: CoroutineContext,
) : JobLoop, CoroutineScope by CoroutineScope(coroutineContext) {
  private val _job = MutableStateFlow(launchJob())
  override val job = _job.asStateFlow()

  private fun launchJob() = launch(start = CoroutineStart.LAZY) {
    println("In job")
  }
}
This program fails with
Copy code
Exception in thread "main" java.lang.IllegalStateException: Job is still new or active: LazyStandaloneCoroutine{New}@544a2ea6
Of course the flaw in this design is that if I start the job lazily via`start = CoroutineStart.LAZY` and someone tries to use the Job in a
CoroutineContext
, then I will get the above exception
But starting lazily is of course desirable to me, so that client code can just call
jobLoop.job.value.start()
when they want the job to start (or in my case, when I want to open the websocket connection. Or it could be used like:
Copy code
jobLoop.job
  .onEach { job ->
    // Maybe do some suspending work beforehand,
    // like fetch another resource, get a fresh token, whatever
    job.start()
  }
  .launchIn(myClientScope)
Does anyone have any advice on how I could achieve what I want? Thanks for any feedback!
Fwiw it has occurred to me that I could introduce some boilerplate to start the job normally, i.e.
launch { }
without the
start
parameter, then immediately suspend, but I would need to introduce something else to suspend on, complicating the design.