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

Hello Kotlin community !

I’m an Android developer switching from Java to Kotlin, and I am planning to use Coroutines to handle asynchronous code as it looks very promising.

Back in Java, to handle asynchronous code I was using the Executor class to execute a time-consuming piece of code in another thread, away from the UI thread. I had an AppExecutors class that I injected in my xxxRepository classes to manage a set of Executor. It looked like this :

public class AppExecutors
{
    private static class DiskIOThreadExecutor implements Executor
    {
        private final Executor mDiskIO;

        public DiskIOThreadExecutor()
        {
            mDiskIO = Executors.newSingleThreadExecutor();
        }

        @Override
        public void execute(@NonNull Runnable command)
        {
            mDiskIO.execute(command);
        }
    }

    private static class MainThreadExecutor implements Executor
    {
        private Handler mainThreadHandler = new Handler(Looper.getMainLooper());

        @Override
        public void execute(@NonNull Runnable command)
        {
            mainThreadHandler.post(command);
        }
    }

    private static volatile AppExecutors INSTANCE;

    private final DiskIOThreadExecutor diskIo;
    private final MainThreadExecutor mainThread;

    private AppExecutors()
    {
        diskIo = new DiskIOThreadExecutor();
        mainThread = new MainThreadExecutor();
    }

    public static AppExecutors getInstance()
    {
        if(INSTANCE == null)
        {
            synchronized(AppExecutors.class)
            {
                if(INSTANCE == null)
                {
                    INSTANCE = new AppExecutors();
                }
            }
        }
        return INSTANCE;
    }

    public Executor diskIo()
    {
        return diskIo;
    }

    public Executor mainThread()
    {
        return mainThread;
    }
}

Then I was able to write some code like this in my xxxRepository :

executors.diskIo().execute(() ->
        {
            try
            {
                LicensedUserOutput license = gson.fromJson(Prefs.getString(Constants.SHAREDPREF_LICENSEINFOS, ""), LicensedUserOutput.class);

                /**
                 * gson.fromJson("") returns null instead of throwing an exception as reported here :
                 * https://github.com/google/gson/issues/457
                 */
                if(license != null)
                {
                    executors.mainThread().execute(() -> callback.onUserLicenseLoaded(license));
                }
                else
                {
                    executors.mainThread().execute(() -> callback.onError());
                }
            }
            catch(JsonSyntaxException e)
            {
                e.printStackTrace();

                executors.mainThread().execute(() -> callback.onError());
            }
        });

It worked very good and Google even has something similar in their many Github Android repo examples.

So I was using callbacks. But now I am tired of the nested callbacks and I want to get rid of them. To do so, I could write in my xxxViewModel for example :

executors.diskIo().execute(() -> 
        {
            int result1 = repo.fetch();
            String result2 = repo2.fetch(result1);
            
            executors.mainThread().execute(() -> myLiveData.setValue(result2));
        });

How is that USAGE different from Kotlin’s coroutines’ usage ? From what I saw, their biggest advantage is to be able to use asynchronous code in a sequential style. But I am able to do just that with Executor, as you can see from the code sample right above.
So what am I missing here ? What would I gain to switch from Executor to Coroutines ?

Note : I get the “coroutine are lightweight” argument, but in Android we don’t have to run 10 000 simultaneous tasks. 4-5 are generally enough.

I had a same question before, but my English is poor.
https://discuss.kotlinlang.org/t/12097

The corresponding code with coroutines would be something like:

launch(Dispatchers.IO) {
    val result1 = repo.fetch()
    val result2 = repo2.fetch(result1)
    withContext(Dispatchers.Main) {
        myLiveData.setValue(result2))
    }
}

First difference is you don’t have lambdas or execute(), so it’s cleaner code. Second is that coroutines give you structured concurrency, which makes it simple to cancel the coroutine. And third, with coroutines you have a bunch of smart people writing and maintaining the library, instead of you writing your own executors.

If you use a library like ktor to implement fetch, then no executors are necessary at all. Your code would simply be:

val result1 = repo.fetch();
val result2 = repo2.fetch(result1);

The main thread is not blocked because execution returns immediately from inside “repo.fetch()” and the the main thread-loop can continue. Later, a task to finish the rest of your method with an actual result1 value will be posted to the main thread-loop and that is when repo2.fetch(result1) is called. The compiler does all the callback handling for you.