А теперь давайте посмотрим на то как происходит не блокирование потока и отсутствие колбэков «под капотом». Если честно, то колбэки все же есть=) Не все так просто, но и не сложно! Корутины используют специальный механизм Continuation.

Continuation можно рассматривать как колбэк. Continuation — похож на огромную стейт-машину, где на каждый кусок Корутин-кода(Suspend-функции) есть свой Switch-Case.

Давайте смотреть код, который мы уже использовали:

launch {
	val url = "http://myurl.com/file.pdf"

	// long synchronous function
	suspendedGetFile(url)

	println("File is downloaded")
}

Давайте посмотрим на Java класс, который получится в результате преобразования Kotlin кода корутины.

class GeneratedContinuationClass extends SuspendLambda {

    int label;
    String url;

    void invokeSuspend() {
        switch (label) {
            case 0: {  // шаг 1-ый
                url = buildUrl();
                label = 1;

                suspendedGetFile(url, this);  // Обратите внимание на "this". Его не было в Kotlin версии
                return;
            }              // <= Примерно тут происходит водораздел нашего кода. До старта suspend-функции и после
            case 1: {  // шаг 2-ой
                println("File is downloaded");
                return;
            }
        }
    }
}

Заметьте, что наша корутина трансформировалась в отдельный класс. Обратите внимание на метод invokeSuspend(), переменную label и на то как она используется в switch-е. Как вы успели заметить, основной код нашей Корутины(buildUrl(),suspendedGetFile(), println()) находится в методе invokeSuspend(). Он вызывается при запуске Корутины.

Continuation нужен для того чтобы код расположенный после suspendedGetFile() — а это println() — был исполнен только после завершения supendedGetFile(). Для этого код делится switch-ем на две части и добавляется переменная label. Точка разделения кода на две части — это suspend-функция.

Основная задача Continuation — сделать так, чтобы код, расположенный после вызова функции download(), был выполнен только когда метод download() завершил работу. Для этого код делится switch-ем на две части. И добавляется переменная label. Точка разделения кода на две части — это suspend-функция. От значения переменной label зависит, какая из двух частей будет выполнена при вызове invokeSuspend(). В нашем примере invokeSuspend() будет вызван 2 раза. Ключевые слова «case» в данном случае стоит понимать как «шаги»

Итак, первый раз invokeSuspend() будет вызван при старте Корутины. Он выполнит блок кода в case0: поменяет значение label на 1 и запустит suspend-функцию. Второй раз invokeSuspend() будет вызван уже из нашей suspend-функции suspendedGetFile(), когда она завершит свою работу. Как так?=) Suspend-функция при преобразовании в Java получает дополнительный входной параметр с типом Continuation. Т.е. в suspend-функцию suspendedGetFile() мы передаем инстанс текущего Continuation-а — this.

Когда suspend-функция suspendedGetFile() закончит загрузку файла, она возьмет Continuation, который ей передали и вызовет его invokeSuspend() метод. label был установлен в 1 (еще до запуска suspend функции), поэтому switch пойдет во вторую ветку и метод toast будет выполнен.

Таким образом Continuation является колбэком для suspend-функции. Если в корутине есть несколько suspend-функций, то Continuation будет колбэком для всех них.

Возврат значения suspend-функцией
Suspend-функция может возвращать какое-либо значение, как результат своей работы. Давайте посмотрим, как эта ситуация обрабатывается в Continuation.

Немного обновим код:

launch {
	val url = "http://myurl.com/file.pdf"

	// long synchronous function
	val file = suspendedGetFile(url)

	println("File is downloaded")

	val size = suspendUnzip(file)

	println("Size is $size")
}

Мы добавили еще одну suspend-функцию suspendUnzip(). При трансформации кода в Java, код будет разделен на три части:
1) suspendedGetFile() и все, что перед ней
2) suspendUnzip() и все, что между ней и suspendedGetFile()
3) все, что после suspendUnzip()

Точки разделения — это suspend-функции.

File file;
Long size;

void invokeSuspend(Object result) {
    switch (label) {
        case 0: {
            url = buildUrl();
            label = 1;

            suspendedGetFile(url, this);
            return;
        }
        case 1: {
            file = (File) result;
            println("File is downloaded");
            label = 2;

            suspendUnzip(file, this);
            return;
        }
        case 2: {
            size = (Long) result
            toast("File is unzipped, size = " + size);
            return;
        }
    }
}

Как мы ранее уже обсудили, suspend-функция по завершении своей работы вызывает метод invokeSuspend(). И именно сюда же она и передает результат своей работы. Для этого у invokeSuspend() есть входной параметр типа Object.

Suspend-функция suspendedGetFile() при вызове invokeSuspend() передаст файл (File). А в ветке case 1 будет выполнено приведение типа Object к типу File.

Suspend-функция suspendUnzip() при вызове invokeSuspend() передаст размер (Long). А в ветке case 2 будет выполнено приведение типа Object к типу Long.