How are Kotlin's Coroutines different from Java's Executor in Android?

Okay, so coroutines are more often compared to threads rather than the tasks that you run on a given thread pool. An Executor is slightly different in that you have something that is managing threads and queueing tasks up to be executed on those threads.

I will also confess that I have only been using Kotlin's courotines and actors solidly for about 6 months, but let us continue.

Async IO

So, I think that one big difference is that running your task in a coroutine will allow you to achieve concurrency on a single thread for an IO task if that task is a truly async IO task that properly yields control while the IO task is still completing. You can achieve very light weight concurrent reads/writes with coroutines in this way. You could launch 10 000 coroutines all reading from disk at the same time on 1 thread and it would happen concurrently. You can read more on async IO here async io wiki

For an Executor service on the other hand, if you had 1 thread in your pool, your multiple IO tasks would execute and block in series on that thread. Even if you were using an async library.

Structured Concurrency

With coroutines and coroutine scope, you get something called structured concurrency. This means that you have to do far less book keeping about the various background tasks you are running so that you can properly do clean up of those tasks if you enter into some error path. With your executor, you would need to keep track of your futures and do the cleanup yourself. Here is a really good article written by one of the kotlin team leads to fully explain this subtlety. Structured Concurrency

Interaction with Actors

Another, probably more niche advantage is that with coroutines, producers and consumers, you can interact with Actors. Actors encapsulate state, and achieve thread safe concurrency through communication rather than through the traditional synchronized tools. Using all of these you can achieve a very light weight and highly concurrent state with very little thread overhead. Executors just do not offer the ability to interact with synchronized state in something like an Actor with say for example 10 000 threads or even 1000 threads. You could happily launch 100 000 coroutines, and if the tasks are suspending and yield control at suitable points, you can achieve some excellent things. You can read more here Shared Mutable state

Light Weight

And finally, just to demonstrate how light weight coroutine concurrency is, I would challenge you to do something like this on an executor and see what the total elapsed time is (this completed in 1160 milliseconds on my machine):

fun main() = runBlocking {
    val start = System.currentTimeMillis()
    val jobs = List(10_000){
        launch {
            delay(1000) // delays for 1000 millis
            print(".")
        }
    }
    jobs.forEach { it.join() }
    val end = System.currentTimeMillis()
    println()
    println(end-start)
}

There are probably other things, but as I said, I am still learning.