When designing an API with a suspend
function, sometimes I want to convey that this function should be called on, say, an IO thread. Other times that it is essential to do so.
Often it seems obvious; for example a database call should be called using Dispatchers.IO
but if it's an interface function, then the caller cannot assume this.
What is the best approach here?
If the suspend
function really must run in a specific context, then declare it directly in the function body.
suspend fun doInIO() = withContext(Dispatchers.IO) {
}
If the caller should be able to change the dispatcher, the function can add the dispatcher as a default parameter.
suspend fun doInIO(context: CoroutineContext = Dispatchers.IO) = withContext(context) {
}
There is no strict mechanism for contracts like that, so you are flexible with choosing the mechanism that suits you and your team.
1) Always use withContext(Dispatcher.IO)
. This is both strict and performant, if a method is invoked from within IO
context it will be fast-path'ed.
2) Naming/annotation-based conventions. You can make an agreement in the team that any method which ends with IO
or has a specific annotation should be invoked with Dispatchers.IO
. This approach works mostly in small teams and only for project-private API. Once you start exporting it as a library/module for other teams such contracts tend to be broken.
3) You can mix the previous approach with a validation:
suspend fun readFile(file: ...) {
require(coroutineContext[ContinuationInterceptor] == Dispatcher.IO) {
"Expected IO dispatcher, but has ${coroutineContext[ContinuationInterceptor]} instead"
}
// read file
}
But this validation works only if you are not wrapping IO dispatcher in some kind of delegate/proxy. In that case, you should make validation aware of such proxies, something like:
fun validateIoDispatcher(dispatcher: ContinuationInterceptor) {
if (dispatcher is Dispatchers.IO) return
if (dispatcher is ProjectSpecificIoDispatcher) return
if (dispatcher is ProjectSpecificWrapperDispatcher) {
validateIoDispatcher(dispatcher.delegate)
} else {
error("Expected IO dispatcher, but has $dispatcher")
}
}
I want to convey that this function should be called on, say, an IO thread. Other times that it is essential to do so.
Not sure what the difference is between "should" and "essential", but having these approaches in mind you can combine it with default method parameters such as suspend fun probablyIO(dispatcher: CoroutineDispatcher = Dispatchers.IO)
or more flexible naming/annotation conventions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With