kotlin_dev_post

Kotlin Coroutines — преобразование callback в suspend функцию

Предположим, что было принято правильное решение в проекте начать использовать Kotlin сопрограммы (корутины). И в проекте до сих пор используется большая кодовая база или крупная библиотека, которая была написана на Java, в которой этих самых корутин нету. А в этой используемой Java библиотеке крутятся свои пулы потоков, которые выполняют много работы в фоновом режиме. И интерфейс у этой Java библиотеки написан в привычном стиле - с использованием обратных вызовов (callbacks).

То есть клиентский код вызывает необходимые методы, передает в них коллбэк, методы которого потом через неопределенное время будут вызваны в неизвестном потоке. Это классический подход в Java для написания асинхронного кода. Но, как известно, сопрограммы Kotlin позволяют удобно писать асинхронный код без явного использования обратных вызовов и строить программу в привычном императивном и последовательном стиле, не разбивая код на отдельные куски. В этой статье приводится простой пример того, как можно преобразовать обратный вызов в suspend функцию, что облегчит использование Java библиотек в клиентском коде на Kotlin, в котором используются сопрограммы. Если коротко, то используйте функцию suspendCoroutine.

Java библиотека

Допустим, имеется следующий код на Java, который имитирует выполнение долгой работы в собственных фоновых потоках:

Имеется класс SomeJavaApi с собственным экзекутором и пулом потоков. У класса два основных метода (successRemoteCall, failedRemoteCall), которые имитируют, например, удачный и неудачный вызов в сеть или удаленную базу данных. В нашем случае, как видно из кода, это вызов приватной функции runTask, которая сначала блокирует поток на определенное время и после выполняет код для Runnable. А Runnable просто вызывает соответствующие методы коллбэка CustomCallback, которые были переданы в successRemoteCall и failedRemoteCall.

Клиентский код на Kotlin

Теперь напишем клиентский код на Kotlin, в котором используется Java библиотека с нашим классом SomeJavaApi и поочередно вызовем основные методы с использованием классического подхода с коллбэками и с помощью корутин, предварительно преобразовав коллбэк в suspend функцию. Полный код приведен ниже:

Имеется функция main, в которой создается объект Java класса SomeJavaApi. Рассмотрим сначала реализацию кода с коллбэком, который написан в функции runDefaultCallback.

Сначала в функции runDefaultCallback, с использованием ключевого слова object, создается объект defaultApiCallback анонимного класса, который реализует интерфейс SomeJavaApi.CustomCallback. Этот объект необходим для обратного вызова, в методах которого просто производится вывод в консоль соответствующего сообщения с указанием идентификатора потока. Далее производится вызов методов successRemoteCall, failedRemoteCall, в которые передается тот самый defaultApiCallback. После вызывается функция delay, которая блокирует текущий поток на определенное время. После выполнения этой функции получаем следующий вывод в терминал:

callback_to_suspend_output_1

Функция успешно выполнилась. Для удачного вызова в качестве результата была получена строка SUCCESSFUL_CALL. Для неудачного вызова был передан объект класса Exception. Стоит обратить внимание на идентификатор потока [Thread id: 10], который был получен в методах объекта обратного вызова. Это идентификатор того единственного фонового потока в пуле экзекутора, который создается в классе SomeJavaApi.

Рассмотрим выполнение того же самого кода, но уже с использованием корутин. Вся магия происходит в функции runSuspendedCallback, в которую передается в качестве аргументов объект класса SomeJavaApi и объект типа CoroutineScope. Как известно корутины в Kotlin должны выполнятся в рамках определенного контекста, поэтому для запуска двух независимых сопрограмм для методов SomeJavaApi необходим скоуп, в котором хранится ссылка на контекст. В данном случае ссылка на скоуп берется из функции runBlocking, которая запускается сразу при старте main. Как видно, в функции runSuspendedCallback происходит запуск двух корутин с помощью конструкции coroutineScope.launch {}.

И теперь самое главное. Для преобразования коллбэка в suspend вид используется функция из стандартной библиотеки suspendCoroutine. Данная функция передает ссылку на объект типа Continuation, через который происходит возобновление приостановленной сопрограммы. В качестве шаблонного типа передается класс, объекты которого должены быть возвращены в случае успешного выполнения вызова. Это тип, который передается в методе onSuccess в обратном вызове. Как видно, в каждой корутине производится вызов локальной функции createOldApiSuspendedCallback, которая создает объект коллбэка SomeJavaApi.CustomCallback, в методах которого производится вызов continuation.resume() - в случае успеха и вызова метода onSuccess у коллбэка и continuation.resumeWithException(exception) - в случае вызова onFailure у коллбэка. И после все как обычно - данный коллбэк передается в методы класса SomeJavaApi. То есть вызовы Java методов с коллбэками просто оборачиваются в конструкцию suspendCoroutine, которая в случае успеха возвращает объект типа String. Это видно при запуске первой сопрограммы, где результат suspendCoroutine присваивается локальной переменной apiCallResult. При втором запуске смысла получать результат нету, потому что вызов oldJavaApi.failedRemoteCall() всегда генерирует исключение. Стоит обратить внимание, что запуск второй корутины обернут в конструкцию try/catch для корректной обработки исключения из вызова oldJavaApi.failedRemoteCall().

Посмотрим на вывод в терминал после выполнения функции runSuspendedCallback():

output_callback_to_suspend_2

Видно, что сначала основной поток добрался до вызова функции joinAll, которая блокирует поток до тех пор, пока перечисленные корутины не завершат свою работу. Далее происходит поочередный запуск корутин, повторяю результат функции runDefaultCallback. Корутины начали свое выполнение в главном потоке с идентификатором [Thread id: 1]. После видно, что происходит последовательный вывод для удачного вызова и неудачного, а именно фоновый поток из пула вызвал метод коллбэка onSuccess (первая строчка, подчеркнутая синим цветом), что можно определить по идентификатору потока, а результат выполнения корутины с выводом SUCCESSFUL_CALL уже выполнился в главном потоке. Такая же ситуация для неудачного вызова, исключение которого было обработано также в главном потоке.

Таким образом, с помощью функции suspendCoroutine можно легко оборачивать callback-вызовы для преобразования их в привычный последовательный вид для сопрограмм Kotlin.

Полезные ссылки:

 

 

 

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">