What is, in your opinion, optimal pattern to use coroutines (and probably other async code) in the code?
-
Wrap any heavy function, like network call in ‘’‘withContext {}’"’ with appropriate dispatcher to the best of your knowledge.
Advantages: encapsulating threading details, signalling, that this function might take some time to launch.
Disadvantages: your function may be already being called on an appropriate Dispatcher, no need to suspend here. Also you take away control of threading from your caller.
-
Use coroutines as high in your code hierarchy as possible. So network calls would be normal non-suspending functions and off-loading from the main thread would be done in a place, where you really need to run some operation async Vs some other operation.
Advantages: Potentially more of your code may be plain synchronous and library independent. No needless context switches, if you need to do some heavy operations one after another. Simpler low level functions.
Disadvantages: your heavy functions will not give you a hint, that they are heavy. Your threading details will leak to the domain code which may or may not be desirable.
Android team seems to go with first option, seeing, that now Room exposes suspend functions with threading encapsulated within.
What is your opinion on the topic?
In my opinion you should wrap everything that will influence main thread badly, such as network, local non-iu procedures. And it depends on the code and it’s influence on iu.
But what if your function is not called on the main thread in the first place? You will end up with effective code looking like this:
withContext (IO) {
runningInBackground()
withContext (IO) {
yourFunctionNotCalledFromUI()
}
}
Will it not impact performance and common sense, those nested withContext calls?
Although there is still an additional object creation? One strategy is to consider private blocking functions and public suspend functions that specify the correct dispatcher. An example, might be if a function calls File.exists()
thousands of times. In this case, maybe it’s better not to call a File.existsSuspending()
extension function instead?
-
Your netwok, or database, or another module must be available from any context with any scenario. Sync, async, even main thread. It’s not module responsibility. Every module has an api. With some universal methods implementation like callable (or another functional interface) you can use it with any method wrapper: coroutines, rx or sync in some scenarios what used by app with sync, async, or parallel with another module method call.
So you would vote for option 2, Alexey? For not wrapping up low level functions into
withContext (IO){}
and pushing threading decisions into domain?
With option 1 you might write:
launch(Dispatchers.Main) {
val foo = getDataFromNet()
val bar = updateDb(foo)
updateUI(bar)
}
suspend fun getDataFromNet(): Foo = withContext(Dispatchers.IO) {...}
suspend fun updateDb(foo: Foo): Bar = withContext(Dispatchers.IO) {...}
With option 2 it would be:
launch(Dispatchers.Main) {
val foo = withContext(Dispatchers.IO) { getDataFromNet() }
val bar = withContext(Dispatchers.IO) { updateDb(foo) }
updateUI(bar)
}
or maybe (option 2b)
launch(Dispatchers.IO) {
val foo = getDataFromNet()
val bar = updateDb(foo)
withContext(Dispatchers.Main) {
updateUI(bar)
}
}
Option 1 is more idiomatic and easier to read.