Используйте Kotlin Coroutines в своем приложении для Android

В этой лаборатории кода вы узнаете, как использовать Kotlin Coroutines в приложении для Android — новый способ управления фоновыми потоками, который может упростить код за счет уменьшения потребности в обратных вызовах. Сопрограммы — это функция Kotlin, которая преобразует асинхронные обратные вызовы для длительных задач, таких как доступ к базе данных или сети, в последовательный код.

Вот фрагмент кода, чтобы дать вам представление о том, что вы будете делать.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

Код на основе обратного вызова будет преобразован в последовательный код с помощью сопрограмм.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

Вы начнете с существующего приложения, созданного с использованием компонентов архитектуры , которое использует стиль обратного вызова для длительных задач.

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

Предпосылки

  • Знакомство с архитектурными компонентами ViewModel , LiveData , Repository и Room .
  • Опыт работы с синтаксисом Kotlin, включая функции расширения и лямбда-выражения.
  • Базовое понимание использования потоков на Android, включая основной поток, фоновые потоки и обратные вызовы.

Что ты будешь делать

  • Вызов кода, написанного с помощью сопрограмм, и получение результатов.
  • Используйте функции приостановки, чтобы сделать асинхронный код последовательным.
  • Используйте launch и runBlocking для управления выполнением кода.
  • Изучите методы преобразования существующих API в сопрограммы с помощью suspendCoroutine .
  • Используйте сопрограммы с компонентами архитектуры.
  • Изучите передовые методы тестирования сопрограмм.

Что вам понадобится

  • Android Studio 3.5 (лаборатория может работать с другими версиями, но некоторые вещи могут отсутствовать или выглядеть иначе).

Если вы столкнетесь с какими-либо проблемами (ошибки в коде, грамматические ошибки, нечеткие формулировки и т. д.) при работе с этой лабораторией кода, сообщите о проблеме по ссылке Сообщить об ошибке в левом нижнем углу лаборатории кода.

Скачать код

Щелкните следующую ссылку, чтобы загрузить весь код для этой лаборатории кода:

Скачать ZIP

... или клонируйте репозиторий GitHub из командной строки с помощью следующей команды:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Часто задаваемые вопросы

Во-первых, давайте посмотрим, как выглядит начальный образец приложения. Следуйте этим инструкциям, чтобы открыть пример приложения в Android Studio.

  1. Если вы загрузили zip-файл kotlin-coroutines , разархивируйте его.
  2. Откройте проект coroutines-codelab в Android Studio.
  3. Выберите модуль start приложения.
  4. Нажмите на исполнять.png кнопку « Выполнить » и либо выберите эмулятор, либо подключите свое устройство Android, которое должно поддерживать Android Lollipop (минимальный поддерживаемый пакет SDK — 21). Должен появиться экран Kotlin Coroutines:

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

Это приложение использует компоненты архитектуры, чтобы отделить код пользовательского интерфейса в MainActivity от логики приложения в MainViewModel . Найдите минутку, чтобы ознакомиться со структурой проекта.

  1. MainActivity отображает пользовательский интерфейс, регистрирует прослушиватели кликов и может отображать Snackbar . Он передает события в MainViewModel и обновляет экран на основе LiveData в MainViewModel .
  2. MainViewModel обрабатывает события в onMainViewClicked и связывается с MainActivity с помощью LiveData.
  3. Executors определяет BACKGROUND, который может запускать вещи в фоновом потоке.
  4. TitleRepository извлекает результаты из сети и сохраняет их в базе данных.

Добавление сопрограмм в проект

Чтобы использовать сопрограммы в Kotlin, вы должны включить библиотеку coroutines-core в build.gradle (Module: app) вашего проекта. Проекты кодовой лаборатории уже сделали это за вас, поэтому вам не нужно делать это, чтобы завершить кодовую лабораторию.

Корутины на Android доступны в виде основной библиотеки и специальных расширений для Android:

  • kotlinx-corouuntines-core — Основной интерфейс для использования сопрограмм в Kotlin
  • kotlinx-coroutines-android — Поддержка основного потока Android в сопрограммах

Стартовое приложение уже включает зависимости в build.gradle. При создании нового проекта приложения вам потребуется открыть build.gradle (Module: app) и добавить в проект зависимости сопрограмм.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

В Android очень важно избегать блокировки основного потока. Основной поток — это единственный поток, который обрабатывает все обновления пользовательского интерфейса. Это также поток, который вызывает все обработчики кликов и другие обратные вызовы пользовательского интерфейса. Таким образом, он должен работать бесперебойно, чтобы гарантировать отличный пользовательский опыт.

Чтобы ваше приложение отображалось пользователю без видимых пауз, основной поток должен обновлять экран каждые 16 мс или чаще , что составляет около 60 кадров в секунду. Многие распространенные задачи занимают больше времени, например анализ больших наборов данных JSON, запись данных в базу данных или получение данных из сети. Таким образом, вызов подобного кода из основного потока может привести к приостановке, зависанию или даже зависанию приложения. И если вы заблокируете основной поток слишком долго, приложение может даже вылететь и отобразить диалоговое окно « Приложение не отвечает ».

Посмотрите видео ниже, чтобы узнать, как сопрограммы решают эту проблему для нас на Android, вводя main-safety.

Шаблон обратного вызова

Одним из шаблонов для выполнения длительных задач без блокировки основного потока являются обратные вызовы. Используя обратные вызовы, вы можете запускать длительные задачи в фоновом потоке. Когда задача завершается, вызывается обратный вызов, чтобы сообщить вам о результате в основном потоке.

Взгляните на пример шаблона обратного вызова.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

Поскольку этот код снабжен аннотацией @UiThread , он должен выполняться достаточно быстро, чтобы выполняться в основном потоке. Это означает, что он должен вернуться очень быстро, чтобы следующее обновление экрана не задерживалось. Однако, поскольку slowFetch потребуется несколько секунд или даже минут, основной поток не может дождаться результата. Обратный вызов show(result) позволяет slowFetch работать в фоновом потоке и возвращать результат, когда он будет готов.

Использование сопрограмм для удаления обратных вызовов

Обратные вызовы — отличный шаблон, однако у них есть несколько недостатков. Код, который интенсивно использует обратные вызовы, может стать трудным для чтения и трудным для понимания. Кроме того, обратные вызовы не позволяют использовать некоторые функции языка, например исключения.

Сопрограммы Kotlin позволяют преобразовывать код на основе обратного вызова в последовательный код. Код, написанный последовательно, обычно легче читать, и в нем могут даже использоваться языковые функции, такие как исключения.

В конце концов, они делают одно и то же: ждут, пока не будет доступен результат от долго выполняющейся задачи, и продолжают выполнение. Однако в коде они выглядят совсем иначе.

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

Например, в приведенном ниже коде обе функции makeNetworkRequest() и slowFetch() являются функциями suspend .

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

Как и в случае с версией с обратным вызовом, makeNetworkRequest должен немедленно вернуться из основного потока, поскольку он помечен @UiThread . Это означает, что обычно он не мог вызывать блокирующие методы, такие как slowFetch . Вот где ключевое слово suspend творит чудеса.

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

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

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

В этом упражнении вы напишете сопрограмму для отображения сообщения после задержки. Для начала убедитесь, что модуль start открыт в Android Studio.

Понимание CoroutineScope

В Kotlin все сопрограммы выполняются внутри CoroutineScope . Область действия контролирует время жизни сопрограмм через свою работу. Когда вы отменяете задание области, она отменяет все сопрограммы, запущенные в этой области. В Android вы можете использовать область для отмены всех запущенных сопрограмм, когда, например, пользователь уходит из Activity или Fragment . Области также позволяют указать диспетчера по умолчанию. Диспетчер контролирует, какой поток запускает сопрограмму.

Для сопрограмм, запускаемых пользовательским интерфейсом, обычно правильно запускать их в Dispatchers.Main , который является основным потоком на Android. Сопрограмма, запущенная в Dispatchers.Main , не будет блокировать основной поток во время приостановки. Поскольку сопрограмма ViewModel почти всегда обновляет пользовательский интерфейс в основном потоке, запуск сопрограмм в основном потоке избавляет вас от дополнительных переключений потока. Сопрограмма, запущенная в основном потоке, может переключать диспетчеров в любое время после ее запуска. Например, он может использовать другой диспетчер для анализа большого результата JSON вне основного потока.

Использование вьюмоделскопе

Библиотека AndroidX lifecycle-viewmodel-ktx добавляет CoroutineScope в ViewModels, настроенную для запуска сопрограмм, связанных с пользовательским интерфейсом. Чтобы использовать эту библиотеку, вы должны включить ее в build.gradle (Module: start) вашего проекта. Этот шаг уже сделан в проектах codelab.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

Библиотека добавляет viewModelScope в качестве функции расширения класса ViewModel . Эта область привязана к Dispatchers.Main и будет автоматически отменена при очистке ViewModel .

Переключиться с потоков на сопрограммы

В MainViewModel.kt найдите следующий TODO вместе с этим кодом:

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

Этот код использует BACKGROUND ExecutorService (определенный в util/Executor.kt ) для запуска в фоновом потоке. Поскольку sleep блокирует текущий поток, пользовательский интерфейс зависает, если он вызывается в основном потоке. Через одну секунду после того, как пользователь нажмет на основное представление, он запрашивает закусочную.

Вы можете увидеть это, удалив BACKGROUND из кода и запустив его снова. Счетчик загрузки не будет отображаться, и через секунду все «перейдет» в конечное состояние.

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

Замените updateTaps этим кодом на основе сопрограммы, который делает то же самое. Вам придется импортировать launch и delay .

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

Этот код делает то же самое, ожидая одну секунду, прежде чем показать закусочную. Однако есть несколько важных отличий:

  1. viewModelScope. launch запустит сопрограмму в viewModelScope . Это означает, что когда задание, которое мы передали в viewModelScope , отменяется, все сопрограммы в этом задании/области действия будут отменены. Если пользователь покинул действие до возврата delay , эта сопрограмма будет автоматически отменена, когда onCleared вызывается после уничтожения ViewModel.
  2. Так как viewModelScope имеет диспетчер Dispatchers.Main по умолчанию, эта сопрограмма будет запущена в основном потоке. Позже мы увидим, как использовать разные потоки.
  3. delay функции является функцией suspend . Это показано в Android Studio с помощью значок в левом желобе. Несмотря на то, что эта сопрограмма выполняется в основном потоке, delay не блокирует поток ни на одну секунду. Вместо этого диспетчер запланирует возобновление сопрограммы через одну секунду при следующем операторе.

Давай, запусти. Когда вы нажимаете на основной вид, вы должны увидеть закусочную через секунду.

В следующем разделе мы рассмотрим, как протестировать эту функцию.

В этом упражнении вы напишете тест для только что написанного кода. В этом упражнении показано, как тестировать сопрограммы, работающие в Dispatchers.Main , с помощью библиотеки kotlinx-coroutines-test . Позже в этой кодовой лаборатории вы реализуете тест, напрямую взаимодействующий с сопрограммами.

Просмотрите существующий код

Откройте MainViewModelTest.kt в папке androidTest .

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

Правило — это способ запуска кода до и после выполнения теста в JUnit. Два правила используются, чтобы мы могли протестировать MainViewModel в тесте вне устройства:

  1. InstantTaskExecutorRule — это правило JUnit, которое настраивает LiveData для синхронного выполнения каждой задачи.
  2. MainCoroutineScopeRule — это пользовательское правило в этой кодовой базе, которое настраивает Dispatchers.Main на использование TestCoroutineDispatcher из kotlinx-coroutines-test . Это позволяет тестам продвигать виртуальные часы для тестирования и позволяет коду использовать Dispatchers.Main в модульных тестах.

В методе setup новый экземпляр MainViewModel создается с использованием тестовых подделок — это поддельные реализации сети и базы данных, предоставленные в начальном коде, чтобы помочь писать тесты без использования реальной сети или базы данных.

Для этого теста подделки нужны только для удовлетворения зависимостей MainViewModel . Позже в этой лаборатории кода вы обновите подделки для поддержки сопрограмм.

Напишите тест, управляющий сопрограммами

Добавьте новый тест, который гарантирует, что касания обновляются через одну секунду после нажатия на основной вид:

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

При вызове onMainViewClicked будет запущена только что созданная нами сопрограмма. Этот тест проверяет, что текст касания остается «0 касаний» сразу после onMainViewClicked , а затем через 1 секунду он обновляется до «1 касания» .

Этот тест использует виртуальное время для управления выполнением сопрограммы, запущенной onMainViewClicked . MainCoroutineScopeRule позволяет приостанавливать, возобновлять или контролировать выполнение сопрограмм, запущенных в Dispatchers.Main . Здесь мы вызываем advanceTimeBy(1_000) , что заставит главный диспетчер немедленно выполнить сопрограммы, возобновление которых запланировано на 1 секунду позже.

Этот тест полностью детерминирован, что означает, что он всегда будет выполняться одинаково. А поскольку он имеет полный контроль над выполнением сопрограмм, запущенных в Dispatchers.Main , ему не нужно ждать одну секунду, пока значение будет установлено.

Запустите существующий тест

  1. Щелкните правой кнопкой мыши имя класса MainViewModelTest в вашем редакторе, чтобы открыть контекстное меню.
  2. В контекстном меню выбрать исполнять.png Запустите «MainViewModelTest»
  3. Для будущих запусков вы можете выбрать эту тестовую конфигурацию в конфигурациях рядом с исполнять.png кнопку на панели инструментов. По умолчанию конфигурация будет называться MainViewModelTest .

Вы должны увидеть прохождение теста! И это должно занять чуть меньше одной секунды.

В следующем упражнении вы узнаете, как преобразовать существующие API-интерфейсы обратного вызова в использование сопрограмм.

На этом шаге вы начнете преобразовывать репозиторий для использования сопрограмм. Для этого мы добавим сопрограммы во ViewModel , Repository , Room и Retrofit .

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

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

Поскольку сетевой запрос управляется UI-событиями, и мы хотим запустить сопрограмму на их основе, естественным местом для начала использования сопрограмм является ViewModel .

Версия обратного вызова

Откройте MainViewModel.kt , чтобы увидеть объявление refreshTitle .

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

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

Эта реализация использует обратный вызов, чтобы сделать несколько вещей:

  • Прежде чем начать запрос, он отображает счетчик загрузки с _spinner.value = true
  • Когда он получает результат, он очищает счетчик загрузки с помощью _spinner.value = false
  • Если он получает ошибку, он указывает закусочной отображать и очищает счетчик.

Обратите внимание, что обратному вызову onCompleted не передается title . Поскольку мы записываем все заголовки в базу данных Room , пользовательский интерфейс обновляется до текущего заголовка, наблюдая за LiveData , который обновляется Room .

В обновлении сопрограмм мы сохраним точно такое же поведение. Рекомендуется использовать наблюдаемый источник данных, например базу данных Room , для автоматического обновления пользовательского интерфейса.

Версия сопрограмм

Давайте перепишем refreshTitle с помощью сопрограмм!

Поскольку она нам понадобится прямо сейчас, давайте сделаем пустую функцию приостановки в нашем репозитории ( TitleRespository.kt ). Определите новую функцию, которая использует оператор suspend , чтобы сообщить Kotlin, что он работает с сопрограммами.

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

Когда вы закончите с этой лабораторией кода, вы обновите ее, чтобы использовать Retrofit и Room для получения нового заголовка и записи его в базу данных с помощью сопрограмм. На данный момент он просто потратит 500 миллисекунд, притворяясь, что выполняет работу, а затем продолжит работу.

В MainViewModel замените версию обратного вызова refreshTitle на ту, которая запускает новую сопрограмму:

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Давайте пройдемся по этой функции:

viewModelScope.launch {

Как и в случае с сопрограммой для обновления количества нажатий, начните с запуска новой сопрограммы в viewModelScope . Это будет использовать Dispatchers.Main , что нормально. Несмотря на то, что refreshTitle будет делать сетевой запрос и запрос к базе данных, он может использовать сопрограммы для предоставления основного безопасного интерфейса. Это означает, что будет безопасно вызывать его из основного потока.

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

Следующие несколько строк кода фактически вызывают refreshTitle в repository .

try {
    _spinner.value = true
    repository.refreshTitle()
}

Прежде чем эта сопрограмма что-либо сделает, она запускает счетчик загрузки, а затем вызывает refreshTitle , как обычную функцию. Однако, поскольку refreshTitle является функцией приостановки, она выполняется не так, как обычная функция.

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

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Исключения в функциях приостановки работают точно так же, как ошибки в обычных функциях. Если вы выдаете ошибку в функции приостановки, она будет передана вызывающей стороне. Таким образом, несмотря на то, что они выполняются совершенно по-разному, для их обработки можно использовать обычные блоки try/catch. Это полезно, потому что позволяет вам полагаться на встроенную поддержку языка для обработки ошибок вместо создания собственной обработки ошибок для каждого обратного вызова.

И если вы выбросите исключение из сопрограммы — эта сопрограмма по умолчанию отменит своего родителя. Это означает, что можно легко отменить несколько связанных задач одновременно.

И затем, в блоке finally, мы можем убедиться, что счетчик всегда выключен после выполнения запроса.

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

В следующем упражнении вы обновите репозиторий, чтобы он действительно работал.

В этом упражнении вы узнаете, как переключить поток, в котором работает сопрограмма, чтобы реализовать рабочую версию TitleRepository .

Просмотрите существующий код обратного вызова в refreshTitle.

Откройте TitleRepository.kt и просмотрите существующую реализацию на основе обратного вызова.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

В TitleRepository.kt метод refreshTitleWithCallbacks реализован с обратным вызовом для сообщения вызывающей стороне о загрузке и состоянии ошибки.

Эта функция делает несколько вещей для реализации обновления.

  1. Переключиться на другой поток с помощью BACKGROUND ExecutorService
  2. Запустите сетевой запрос fetchNextTitle , используя блокирующий метод execute() . Это запустит сетевой запрос в текущем потоке, в данном случае в одном из потоков в BACKGROUND .
  3. Если результат успешен, сохраните его в базе данных с помощью insertTitle и вызовите метод onCompleted() .
  4. Если результат не удался или есть исключение, вызовите метод onError, чтобы сообщить вызывающей стороне о неудачном обновлении.

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

Вызов блокировки вызовов из сопрограмм

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

Вы можете использовать этот шаблон в любое время, когда вам нужно выполнить блокировку или работу с интенсивным использованием ЦП внутри сопрограммы, например, сортировку и фильтрацию большого списка или чтение с диска.

Для переключения между любыми диспетчерами сопрограммы используют withContext . Вызов withContext переключается на другой диспетчер только для лямбды, а затем возвращается к диспетчеру, вызвавшему его, с результатом этой лямбды.

По умолчанию сопрограммы Kotlin предоставляют три диспетчера: Main , IO и Default . Диспетчер ввода-вывода оптимизирован для операций ввода-вывода, таких как чтение из сети или с диска, в то время как диспетчер по умолчанию оптимизирован для задач с интенсивным использованием ЦП.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

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

Этот код по-прежнему использует блокировку вызовов. Вызов execute() и insertTitle(...) заблокирует поток, в котором выполняется эта сопрограмма. Однако, переключаясь на Dispatchers.IO с помощью withContext , мы блокируем один из потоков в диспетчере ввода-вывода. Вызвавшая это сопрограмма, возможно работающая на Dispatchers.Main , будет приостановлена ​​до тех пор, пока лямбда withContext не будет завершена.

По сравнению с версией с обратным вызовом есть два важных отличия:

  1. withContext возвращает результат обратно вызывавшему его Dispatcher, в данном случае Dispatchers.Main . Версия обратного вызова вызывала обратные вызовы в потоке службы исполнителя BACKGROUND .
  2. Вызывающий не должен передавать обратный вызов этой функции. Они могут полагаться на приостановку и возобновление, чтобы получить результат или ошибку.

Запустите приложение снова

Если вы снова запустите приложение, вы увидите, что новая реализация на основе сопрограмм загружает результаты из сети!

На следующем шаге вы интегрируете сопрограммы в Room и Retrofit.

Чтобы продолжить интеграцию сопрограмм, мы воспользуемся поддержкой функций приостановки в стабильной версии Room и Retrofit, а затем существенно упростим код, который мы только что написали, используя функции приостановки.

Корутины в комнате

Сначала откройте MainDatabase.kt и сделайте insertTitle функцией приостановки:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Когда вы это сделаете, Room сделает ваш запрос безопасным и автоматически выполнит его в фоновом потоке. Однако это также означает, что вы можете вызывать этот запрос только из сопрограммы.

И это все, что вам нужно сделать, чтобы использовать сопрограммы в Room. Довольно изящно.

Корутины в модификации

Далее давайте посмотрим, как интегрировать сопрограммы с Retrofit. Откройте MainNetwork.kt и измените fetchNextTitle на функцию приостановки.

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Чтобы использовать функции приостановки с Retrofit, вам нужно сделать две вещи:

  1. Добавьте модификатор приостановки в функцию
  2. Удалите оболочку Call из возвращаемого типа. Здесь мы возвращаем String , но вы также можете вернуть сложный тип на основе json. Если вы по-прежнему хотите предоставить доступ к полному Result модификации, вы можете вернуть Result<String> вместо String из функции приостановки.

Retrofit автоматически сделает функции приостановки безопасными для основной среды , чтобы вы могли вызывать их непосредственно из Dispatchers.Main .

Использование комнаты и модернизации

Теперь, когда Room и Retrofit поддерживают функции приостановки, мы можем использовать их из нашего репозитория. Откройте TitleRepository.kt и посмотрите, как использование приостанавливающих функций значительно упрощает логику, даже по сравнению с блокирующей версией:

Заголовок Repository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Вау, это намного короче. Что случилось? Оказывается, использование приостановки и возобновления позволяет сделать код намного короче. Модернизация позволяет нам использовать возвращаемые типы, такие как String или объект User , вместо Call . Это безопасно, потому что внутри функции приостановки Retrofit может запускать сетевой запрос в фоновом потоке и возобновлять сопрограмму после завершения вызова.

Более того, мы избавились от withContext . Поскольку и Room, и Retrofit предоставляют основные безопасные функции приостановки, можно безопасно организовать эту асинхронную работу из Dispatchers.Main .

Исправление ошибок компилятора

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

Пройдитесь по проекту и исправьте ошибки компилятора, изменив функцию на приостановку созданной. Вот быстрые решения для каждого:

TestingFakes.kt

Обновите подделки тестирования, чтобы они поддерживали новые модификаторы приостановки.

НазваниеDaoFake

  1. Нажмите alt-enter, чтобы добавить модификаторы приостановки ко всем функциям в иерархии.

ГлавнаяNetworkFake

  1. Нажмите alt-enter, чтобы добавить модификаторы приостановки ко всем функциям в иерархии.
  2. Замените fetchNextTitle этой функцией
override suspend fun fetchNextTitle() = result

MainNetworkCompletedFake

  1. Нажмите alt-enter, чтобы добавить модификаторы приостановки ко всем функциям в иерархии.
  2. Замените fetchNextTitle этой функцией
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Удалите функцию refreshTitleWithCallbacks , так как она больше не используется.

Запустите приложение

Run the app again, once it compiles, you will see that it's loading data using coroutines all the way from the ViewModel to Room and Retrofit!

Congratulations, you've completely swapped this app to using coroutines! To wrap up we'll talk a bit about how to test what we just did.

In this exercise, you'll write a test that calls a suspend function directly.

Since refreshTitle is exposed as a public API it will be tested directly, showing how to call coroutines functions from tests.

Here's the refreshTitle function you implemented in the last exercise:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Write a test that calls a suspend function

Open TitleRepositoryTest.kt in the test folder which has two TODOS.

Try to call refreshTitle from the first test whenRefreshTitleSuccess_insertsRows .

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

Since refreshTitle is a suspend function Kotlin doesn't know how to call it except from a coroutine or another suspend function, and you will get a compiler error like, "Suspend function refreshTitle should be called only from a coroutine or another suspend function."

The test runner doesn't know anything about coroutines so we can't make this test a suspend function. We could launch a coroutine using a CoroutineScope like in a ViewModel , however tests need to run coroutines to completion before they return. Once a test function returns, the test is over. Coroutines started with launch are asynchronous code, which may complete at some point in the future. Therefore to test that asynchronous code, you need some way to tell the test to wait until your coroutine completes. Since launch is a non-blocking call, that means it returns right away and can continue to run a coroutine after the function returns - it can't be used in tests. Например:

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

This test will sometimes fail. The call to launch will return immediately and execute at the same time as the rest of the test case. The test has no way to know if refreshTitle has run yet or not – and any assertions like checking that the database was updated would be flakey. And, if refreshTitle threw an exception, it will not be thrown in the test call stack. It will instead be thrown into GlobalScope 's uncaught exception handler.

The library kotlinx-coroutines-test has the runBlockingTest function that blocks while it calls suspend functions. When runBlockingTest calls a suspend function or launches a new coroutine, it executes it immediately by default. You can think of it as a way to convert suspend functions and coroutines into normal function calls.

In addition, runBlockingTest will rethrow uncaught exceptions for you. This makes it easier to test when a coroutine is throwing an exception.

Implement a test with one coroutine

Wrap the call to refreshTitle with runBlockingTest and remove the GlobalScope.launch wrapper from subject.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

This test uses the fakes provided to check that "OK" is inserted to the database by refreshTitle .

When the test calls runBlockingTest , it will block until the coroutine started by runBlockingTest completes. Then inside, when we call refreshTitle it uses the regular suspend and resume mechanism to wait for the database row to be added to our fake.

After the test coroutine completes, runBlockingTest returns.

Write a timeout test

We want to add a short timeout to the network request. Let's write the test first then implement the timeout. Create a new test:

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

This test uses the provided fake MainNetworkCompletableFake , which is a network fake that's designed to suspend callers until the test continues them. When refreshTitle tries to make a network request, it'll hang forever because we want to test timeouts.

Then, it launches a separate coroutine to call refreshTitle . This is a key part of testing timeouts, the timeout should happen in a different coroutine than the one runBlockingTest creates. By doing so, we can call the next line, advanceTimeBy(5_000) which will advance time by 5 seconds and cause the other coroutine to timeout.

This is a complete timeout test, and it will pass once we implement timeout.

Run it now and see what happens:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

One of the features of runBlockingTest is that it won't let you leak coroutines after the test completes. If there are any unfinished coroutines, like our launch coroutine, at the end of the test, it will fail the test.

Add a timeout

Open up TitleRepository and add a five second timeout to the network fetch. You can do this by using the withTimeout function:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Run the test. When you run the tests you should see all tests pass!

In the next exercise you'll learn how to write higher order functions using coroutines.

In this exercise you'll refactor refreshTitle in MainViewModel to use a general data loading function. This will teach you how to build higher order functions that use coroutines.

The current implementation of refreshTitle works, but we can create a general data loading coroutine that always shows the spinner. This might be helpful in a codebase that loads data in response to several events, and wants to ensure the loading spinner is consistently displayed.

Reviewing the current implementation every line except repository.refreshTitle() is boilerplate to show the spinner and display errors.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Using coroutines in higher order functions

Add this code to MainViewModel.kt

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Now refactor refreshTitle() to use this higher order function.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

By abstracting the logic around showing a loading spinner and showing errors, we've simplified our actual code needed to load data. Showing a spinner or displaying an error is something that's easy to generalize to any data loading, while the actual data source and destination needs to be specified every time.

To build this abstraction, launchDataLoad takes an argument block that is a suspend lambda. A suspend lambda allows you to call suspend functions. That's how Kotlin implements the coroutine builders launch and runBlocking we've been using in this codelab.

// suspend lambda

block: suspend () -> Unit

To make a suspend lambda, start with the suspend keyword. The function arrow and return type Unit complete the declaration.

You don't often have to declare your own suspend lambdas, but they can be helpful to create abstractions like this that encapsulate repeated logic!

In this exercise you'll learn how to use coroutine based code from WorkManager.

What is WorkManager

There are many options on Android for deferrable background work. This exercise shows you how to integrate WorkManager with coroutines. WorkManager is a compatible, flexible and simple library for deferrable background work. WorkManager is the recommended solution for these use cases on Android.

WorkManager is part of Android Jetpack , and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that WorkManager will do your background work as soon as it can. Guaranteed execution means that WorkManager will take care of the logic to start your work under a variety of situations, even if you navigate away from your app.

Because of this, WorkManager is a good choice for tasks that must complete eventually.

Some examples of tasks that are a good use of WorkManager:

  • Uploading logs
  • Applying filters to images and saving the image
  • Periodically syncing local data with the network

Using coroutines with WorkManager

WorkManager provides different implementations of its base ListanableWorker class for different use cases.

The simplest Worker class allows us to have some synchronous operation executed by WorkManager. However, having worked so far to convert our codebase to use coroutines and suspend functions, the best way to use WorkManager is through the CoroutineWorker class that allows to define our doWork() function as a suspend function.

To get started, open up RefreshMainDataWork . It already extends CoroutineWorker , and you need to implement doWork .

Inside the suspend doWork function, call refreshTitle() from the repository and return the appropriate result!

After you've completed the TODO, the code will look like this:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Note that CoroutineWorker.doWork() is a suspending function. Unlike the simpler Worker class, this code does NOT run on the Executor specified in your WorkManager configuration, but instead use the dispatcher in coroutineContext member (by default Dispatchers.Default ).

Testing our CoroutineWorker

No codebase should be complete without testing.

WorkManager makes available a couple of different ways to test your Worker classes, to learn more about the original testing infrastructure, you can read the documentation .

WorkManager v2.1 introduces a new set of APIs to support a simpler way to test ListenableWorker classes and, as a consequence, CoroutineWorker. In our code we're going to use one of these new API: TestListenableWorkerBuilder .

To add our new test, update the RefreshMainDataWorkTest file under the androidTest folder.

The content of the file is:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Before we get to the test, we tell WorkManager about the factory so we can inject the fake network.

The test itself uses the TestListenableWorkerBuilder to create our worker that we can then run calling the startWork() method.

WorkManager is just one example of how coroutines can be used to simplify APIs design.

In this codelab we have covered the basics you'll need to start using coroutines in your app!

We covered:

  • How to integrate coroutines to Android apps from both the UI and WorkManager jobs to simplify asynchronous programming,
  • How to use coroutines inside a ViewModel to fetch data from the network and save it to a database without blocking the main thread.
  • And how to cancel all coroutines when the ViewModel is finished.

For testing coroutine based code, we covered both by testing behavior as well as directly calling suspend functions from tests.

Learn more

Check out the " Advanced Coroutines with Kotlin Flow and LiveData " codelab to learn more advanced coroutines usage on Android.

Kotlin coroutines have many features that weren't covered by this codelab. If you're interested in learning more about Kotlin coroutines, read the coroutines guides published by JetBrains. Also check out " Improve app performance with Kotlin coroutines " for more usage patterns of coroutines on Android.