One of the reasons I love and use kotlin’s coroutines is because they allow you to write asynchronous code in a synchronous way. The syntax is really beautiful :
fun doSomething() = launch {
val result = async { getWhatever() }.await()
// Do something with result.
}
But in android you will probably use kotlinx-coroutines-android
and you will have to specify the UI
CoroutineContext:
fun doSomething() = launch(UI) {
val result = async(CommonPool) { getWhatever() }.await()
// Do something with result.
}
But you won’t be able to unit test any coroutine that uses the UI
CoroutineContext. So you will end up injecting some ContextProvider and specifying the CoroutineContext whenever you use launch and async. The syntax isn’t that beautiful anymore
fun doSomething() = launch(contextProvider.MAIN) {
val result = async(contextProvider.IO) { getWhatever() }.await()
// Do something with result.
}
what comes next is just a question I want to raise… What about having your own launch
and async
functions around the coroutines original functions and a static object that holds the CoroutineContexts?
object CoroutineContext {
var MAIN: CoroutineContext = UI
var IO: CoroutineContext = CommonPool
}
fun launch(block: suspend CoroutineScope.() -> Unit) = launch(MAIN, block = block)
fun <T> async(block: suspend CoroutineScope.() -> T) = async(IO, block = block)
And whenever you want to run and test your code synchronously you set whatever CoroutineContext you want:
@BeforeTest
fun setUp() {
CoroutineContext.MAIN = Unconfined
CoroutineContext.IO = Unconfined
}
Suddenly that beautiful syntax is back! What doesn’t smell good is that static object… But the syntax…
¿Is this a good approach? ¿Would you use it? ¿Why? ¿Why not?
2 Likes
I don’t see any reason to not use something like this. Yes, in most cases I’m not a fan of global objects but there are always exceptions. I think this is one such situation. The thing I don’t like about this is that both MAIN
and IO
are variables and not read only.
It might be possible to define them as val
and then use mocking to change the coroutine context during debugging. That way it is not possible to change this global state, which could lead to some real nasty bugs. But on the other hand, why would anyone change this at runtime? So I’m not even sure it is worth using val
instead.
The reason I use var
for the CoroutineContexts is because if you use val
you would have to use reflection to mock those properties for testing.
I know global objects are awful and mutable global properties are even more awful. But losing the beautiful syntax bothers me quite a lot. I just wanted to open a debate over if keeping the beautiful syntax justifies doing those awful two things.
Yes, I think in this case it does. I don’t think that objects are inherently bad. The problem is that they often lead to code that does not scale well in complexity. That does not mean though that there are no god reasons to sometimes use singletons.
This code does not really need to scale, no one will ever change those variables and they overall lead to more readable code so yeah, I think in this situation your solution is better than just always passing the coroutine context around.
Another advantage of this is, that if you need to you can easily switch all coroutines to use a single thread for debugging.
Written on phone.
Consider using this:
class MyCoroutineContext(val main: CoroutineContext, val io: CoroutineContext) {
fun <T> asyncio(block: CoroutineScope.() -> T) {...}
... More functions
}
Then:
with(MyCoroutineContext(MAIN, IO)) c@ {
launch {
val obj = asyncio { ... }
// Do stuff
}
}
Oh yeah, that should be a suspended function param.
Of course you can shorten the with(...) { launch { } } part
with some helper function.