Котлин корутины. Часть 4. Переход callback API на корутины

В третьей части этой серии вы узнали, как тестировать корутины через поведение. В этой части мы конвертируем существующий API, который работает на основе методов обратного вызова (колбеков).  Заменим колбеки на корутины .

Чтобы на практике увидеть работу с Kotlin Coroutines, Room и архитектурными компонентами, записывайтесь на продвинутый курс по разработке приложения «Чат-мессенжер»

Откройте проект в Android Studio

Для начала откройте проект kotlin-coroutines-repository в Android Studio. Исходный код можно найти в первом уроке.

Это приложение использует компоненты архитектуры и расширяет предыдущий проект для реализации уровня данных, который использует как сетевую, так и локальную базу данных. При щелчке по основному виду он выбирает новый заголовок из сети, сохраняет его в базе данных и отображает его на экране. Найдите минутку, чтобы ознакомиться с новыми классами.

  1. MainDatabase реализует базу данных с использованием Room, которая сохраняет и загружает Title.
  2. MainNetwork реализует сетевой API, который выбирает новый заголовок. Он использует поддельную сетевую библиотеку, определенную в FakeNetworkLibrary.kt для того, чтобы получать названия. Сетевая библиотека будет случайным образом возвращать ошибки.
  3. TitleRepository реализует единый API для извлечения или обновления заголовка путем объединения данных из сети и базы данных.
  4. MainViewModelTest определяет тест для MainViewModel.
  5. FakeNetworkCallAwaitTest это тест, который мы закончим позже в этом уроке.

Исследуйте существующий callback API

Откройте MainNetwork.kt и посмотрите на декларацию fetchNewWelcome()

// MainNetwork.kt

fun fetchNewWelcome(): FakeNetworkCall<String>

Откройте TitleRepository.kt чтобы увидеть, как fetchNewWelcome используется для выполнения сетевого вызова с использованием колбеков.

Эта функция возвращает FakeNetworkCall, что позволяет регистрировать слушателя для этого сетевого запроса. Вызов fetchNewWelcome запустит длительный сетевой запрос в другом потоке и вернет объект, который устанавливает addOnResultListener. Ваш код передает обратный вызов addOnResultListener который будет вызван, когда запрос завершится успехом или ошибкой.

// TitleRepository.kt

fun refreshTitle(/* ... */) {
   val call = network.fetchNewWelcome()
   call.addOnResultListener { result ->
       // callback called when network request completes or errors
       when (result) {
           is FakeNetworkSuccess<String> -> {
               // process successful result
           }
           is FakeNetworkError -> {
               // process network error
           }
       }
   }
}

Преобразование callback API в suspend функцию

Функция refreshTitle в настоящее время реализована с использованием колбеков на FakeNetworkCall. Цель этого упражнения — представить наш сетевой API-интерфейс как suspend функцию, чтобы refreshTitle можно было переписать с использованием корутин.

Для этого Kotlin предоставляет функцию suspendCoroutine, которая используется для преобразования API на основе колбеков в suspend функции.

Вызов suspendCoroutine немедленно приостановит текущую корутину suspendCoroutine и предоставит вам объект continuation, который вы можете использовать для возобновления корутины. Continuation содержит весь контекст, необходимый для продолжения или возобновления приостановленной корутины.

Continuation , которое предоставляет suspendCoroutine, имеет две функции: resume и resumeWithException. Вызов любой функции приведет к немедленному возобновлению suspendCoroutine.

Вы можете использовать suspendCoroutine для приостановки перед ожиданием обратного вызова. Затем после колбека вызывается resume вызов или resumeWithException для возобновления с результатом колбека.

Пример suspendCoroutine выглядит следующим образом:

// Example of suspendCoroutine

/**
 * A class that passes strings to callbacks
 */
class Call {
  fun addCallback(callback: (String) -> Unit)
}

/**
 * Exposes callback based API as a suspend function so it can be used in coroutines.
 */
suspend fun convertToSuspend(call: Call): String {
   // 1: suspendCoroutine and will immediately *suspend* 
   // the coroutine. It can be only *resumed* by the
   // continuation object passed to the block.
   return suspendCoroutine { continuation ->
       // 2: pass a block to suspendCoroutine to register callbacks

       // 3: add a callback to wait for the result
       call.addCallback { value ->
           // 4: use continuation.resume to *resume* the coroutine
           // with the value. The value passed to resume will be
           // the result of suspendCoroutine.
           continuation.resume(value)
       }
   }
 }

В этом примере показано, как использовать suspendCoroutine для преобразования API на основе обратного вызова при вызове в функцию приостановки. Теперь вы можете использовать Call непосредственно в коде на основе сопрограмм, например:

// Example of using convertToSuspend to use a callback API in coroutines

suspend fun exampleUsage() {
    val call = makeLongRunningCall()
    convertToSuspend(call) // suspends until the long running call completes
}

Вы можете использовать этот шаблон для предоставления функции приостановки на FakeNetworkCall, которая позволяет вам использовать сетевой API на основе колбеков в корутинах.

Как насчет отмены?

suspendCoroutine — хороший выбор, когда вам не нужно поддерживать cancellation. Как правило, однако, cancellation является проблемой, и вы можете использовать suspendCancellableCoroutine для распространения cancellation в библиотеки, которые поддерживают cancellation в API на основе колбеков.

suspendCoroutine для преобразования callback API в корутины

Прокрутите до конца TitleRepository.kt и найдите TODO для реализации функции расширения.

/**
* Suspend function to use callback-based [FakeNetworkCall] in coroutines
*
* @return network result after completion
* @throws Throwable original exception from library if network request fails
*/
// TODO: Implement FakeNetworkCall<T>.await() here

Замените этот TODO этой функцией расширения на FakeNetworkCall <T>:

suspend fun <T> FakeNetworkCall<T>.await(): T {
   return suspendCoroutine { continuation ->
       addOnResultListener { result ->
           when (result) {
               is FakeNetworkSuccess<T> -> continuation.resume(result.data)
               is FakeNetworkError -> continuation.resumeWithException(result.error)
           }
       }
   }
}

Эта suspend функция использует suspendCoroutine для преобразования API на основе колбеков в suspend функцию. Корутины могут вызвать await и будут немедленно приостановлены, пока результат сети не будет готов. Результатом сети является возвращаемое значение await, и ошибки будут вызывать исключение.

Вы можете использовать это так:

// Example usage of await

suspend fun exampleAwaitUsage() {
   try {
       val call = network.fetchNewWelcome()
       // suspend until fetchNewWelcome returns a result or throws an error
       val result = call.await()
       // resume will cause await to return the network result
   } catch (error: FakeNetworkException) {
       // resumeWithException will cause await to throw the error
   }
}

Стоит потратить секунду, чтобы прочитать сигнатуру suspend функции. Ключевое слово suspend сообщает Kotlin, что это доступно корутинам. В результате он может вызывать другие suspend функции, такие как suspendCoroutine. Остальная часть объявления, fun <T> FakeNetworkCall <T> .await (), определяет функцию расширения, которая вызывает await для любого FakeNetworkCall. На самом деле он не изменяет класс, но при вызове из Kotlin он отображается как public метод. Тип возвращаемого значения await — T, который указывается после имени функции.

Что такое функция расширения?

Если вы новичок в Kotlin, функции расширения могут быть новой концепцией. Функции расширения не модифицируют класс, вместо этого они вводят новую функцию, которая принимает this в качестве первого аргумента.

fun <T> await(this: FakeNetworkCall<T>): T

Внутри тела await функции, this связан с переданным FakeNetworkCall<T> . Вот как await  вызывает addOnResultListener. Он использует неявный this так же, как метод члена.

Все вместе эта подпись означает, что мы добавляем функцию приостановки, называемую await (), в класс, который изначально не был создан для корутин. Этот подход можно использовать для обновления API на основе колбеков для поддержки корутин без изменения реализации.

В следующем упражнении мы напишем тесты для await () и узнаем, как вызывать корутины непосредственно из тестов.

Понравилась статья? Поделиться с друзьями:
Комментарии: 1
  1. danilukvalera

    когда будет продолжение?

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