Coroutines is a Kotlin concept. And you're not sending your task to "the main thread" but to the Main coroutine Dispatcher. (Although behind the scenes you'll see that indeed you're calling that code from the main thread).
Kotlin is just a programming language that later compiles to different other languages (JVM bytecode for Java and Android, LLVM for native targets and JS for Browser or NodeJS targets).
Regardless of the target the concept remains the same: Kotlin coroutines.
For example: in the JavaScript world we don't have threads: we just have the synchronous and asynchronous task stacks. Same story in case we compiled for embedded devices like Arduino or ESP32's.
For Android coroutines implementation it uses the Executors
, Handlers
and Looper
API's. It doesn't "magically inject itself into the Main thread". But the code you've provided ends up doing something as follows:
Handler(Looper.getMainLooper()).post({/*your lambda here*/})
So the Main
coroutine dispatcher on Android will eventually dispatch (pass) a task (your lambda) to the Main Looper on Android.
If that code were to be run on the browser (JavaScript) it'd be a whole other thing at low-level implementation. I imagine it would be something like this (but we'd have to check the source code at github):
new Promise((res) => res(yourLambda()))
But the concept of the coroutines remains the same: We're passing a task to the Main coroutine dispatcher.
Finally, answering your question:
Why can you run a Kotlin coroutine on the main thread?
There's code (or tasks) that are safe to run (or must be run) on the Main thread: like manipulating the properties of a View for instance. To do that we delegate those tasks to the Main coroutine dispatcher that will eventually run your code on the Main thread.
This code is valid to be run on the Main thread; yet it's marked as suspending code. Same happens with your logging statements. They are valid to be run in a suspending context or out of it.
suspend fun makeGone(view: View) {
view.visibility = View.GONE
}
The fact that a function is marked as suspend
does not really matter: it only requires you to run it inside a CoroutineContext
or CoroutineScope
.
Now with this said: keep in mind that Android will crash if you run IO code from the Main thread:
override fun onCreate() {
launch(Dispatcher.Main) { somethingThatDoesIO() }
}
This code will crash even if somethingThatDoesIO
is not marked as suspend as it's performing IO operations on the Main thread.
if we re-implement the somethingThatDoesIO
as follows:
suspend fun somethingThatDoesIO() = withContext(Dispatchers.IO) {
// old code
}
What's gonna happen is:
- You dispatch a task to the Main coroutine dispatcher
- The main coroutine dispatcher will execute the lambda
- It'll realise that
somethingThatDoesIO
needs to be run by the IO dispatcher and "hand over everything it needs" (I know this is very high level)
- When
Dispatchers.IO
is done running the task it's going to give the control to the Main dispatcher which will continue with the code from your lambda.
I'm not sure if I helped you out or made things even more confusing. Let me know in the comments 😅
Dispatchers
were something else, but reading the documentation (via IDE) it explicitly says it will run on the main thread where UI manipulation is available, this is very interesting. – Upward