В этой практической работе вы узнаете, как использовать сопрограммы Kotlin в приложении для 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
Вы начнете с существующего приложения, созданного с использованием Architecture Components , которое использует стиль обратного вызова для длительно выполняемых задач.
К концу этой практической работы вы приобретёте достаточно опыта, чтобы использовать корутины в своём приложении для загрузки данных из сети, и сможете интегрировать корутины в приложение. Вы также познакомитесь с рекомендациями по использованию корутинов и научитесь писать тесты для кода, использующего корутины.
Предпосылки
- Знакомство с компонентами архитектуры
ViewModel
,LiveData
,Repository
иRoom
. - Опыт работы с синтаксисом Kotlin, включая функции расширения и лямбда-выражения.
- Базовые знания об использовании потоков в Android, включая основной поток, фоновые потоки и обратные вызовы.
Что ты будешь делать?
- Вызовите код, написанный с использованием сопрограмм, и получите результаты.
- Используйте функции приостановки, чтобы сделать асинхронный код последовательным.
- Используйте
launch
иrunBlocking
для управления выполнением кода. - Изучите методы преобразования существующих API в сопрограммы с помощью
suspendCoroutine
. - Используйте сопрограммы с компонентами архитектуры.
- Изучите передовой опыт тестирования сопрограмм.
Что вам понадобится
- Android Studio 3. 5 (эта лабораторная работа может работать и с другими версиями, но некоторые вещи могут отсутствовать или выглядеть иначе).
Если во время работы над этой лабораторной работой у вас возникнут какие-либо проблемы (ошибки кода, грамматические ошибки, неясные формулировки и т. д.), сообщите о них, воспользовавшись ссылкой «Сообщить об ошибке» в левом нижнем углу лабораторной работы.
Загрузить код
Чтобы загрузить весь код для этой лабораторной работы, щелкните следующую ссылку:
... или клонируйте репозиторий GitHub из командной строки, используя следующую команду:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Часто задаваемые вопросы
Для начала посмотрим, как выглядит начальный пример приложения. Следуйте этим инструкциям, чтобы открыть пример приложения в Android Studio.
- Если вы загрузили zip-файл
kotlin-coroutines
, разархивируйте его. - Откройте проект
coroutines-codelab
в Android Studio. - Выберите
start
модуль приложения. - Нажмите на
Нажмите кнопку «Выполнить» и выберите эмулятор или подключите Android-устройство, на котором должна быть установлена операционная система Android Lollipop (минимальная поддерживаемая версия SDK — 21). Должен появиться экран Kotlin Coroutines:
Это стартовое приложение использует потоки для увеличения счётчика с небольшой задержкой после нажатия на экран. Оно также получает новый заголовок из сети и отображает его на экране. Попробуйте прямо сейчас, и вы увидите, как счётчик и сообщение изменятся после небольшой задержки. В этой лабораторной работе вы адаптируете это приложение для использования сопрограмм.
Это приложение использует компоненты архитектуры для разделения кода пользовательского интерфейса в MainActivity
от логики приложения в MainViewModel
. Уделите немного времени, чтобы ознакомиться со структурой проекта.
-
MainActivity
отображает пользовательский интерфейс, регистрирует прослушиватели кликов и может отображатьSnackbar
. Он передаёт события вMainViewModel
и обновляет экран на основеLiveData
вMainViewModel
. -
MainViewModel
обрабатывает события вonMainViewClicked
и взаимодействует сMainActivity
с помощьюLiveData.
-
Executors
определяютBACKGROUND,
который может запускать процессы в фоновом потоке. -
TitleRepository
извлекает результаты из сети и сохраняет их в базе данных.
Добавление сопрограмм в проект
Чтобы использовать сопрограммы в Kotlin, необходимо включить библиотеку coroutines-core
в файл build.gradle (Module: app)
вашего проекта. В проектах практического занятия это уже сделано, поэтому вам не нужно делать это для завершения практического занятия.
Сопрограммы на Android доступны в виде базовой библиотеки и специфичных для Android расширений:
- kotlinx-corountines-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 из основного потока.
Использование viewModelScope
Библиотека AndroidX lifecycle-viewmodel-ktx
добавляет к ViewModels объект CoroutineScope, настроенный на запуск сопрограмм, связанных с пользовательским интерфейсом. Чтобы использовать эту библиотеку, необходимо включить её в файл 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
блокирует текущий поток, при вызове в основном потоке пользовательский интерфейс заморозится. Через секунду после того, как пользователь щёлкнет по главному представлению, он запрашивает снэк-бар.
Вы можете увидеть это, удалив ФОНОВЫЙ режим из кода и запустив его снова. Индикатор загрузки не отобразится, и всё «перепрыгнет» в конечное состояние через секунду.
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")
}
}
Этот код делает то же самое, ожидая одну секунду перед отображением снэк-бара. Однако есть несколько важных отличий:
-
viewModelScope.
launch
запустит сопрограмму вviewModelScope
. Это означает, что при отмене задания, переданногоviewModelScope
, все сопрограммы в этом задании/области будут отменены. Если пользователь покинул Activity до возвратаdelay
, эта сопрограмма будет автоматически отменена при вызовеonCleared
после уничтожения ViewModel. - Поскольку
viewModelScope
имеет диспетчер по умолчаниюDispatchers.Main
, эта сопрограмма будет запущена в основном потоке. Позже мы рассмотрим, как использовать другие потоки. - Функция
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 вне устройства используются два правила:
-
InstantTaskExecutorRule
— это правило JUnit, которое настраиваетLiveData
для синхронного выполнения каждой задачи. -
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 касание» .
В этом тесте используется виртуальное время для управления выполнением сопрограммы, запущенной onMainViewClicked
. Правило MainCoroutineScopeRule
позволяет приостанавливать, возобновлять или управлять выполнением сопрограмм, запущенных в Dispatchers.Main
. Здесь мы вызываем advanceTimeBy(1_000)
, что заставляет основной диспетчер немедленно выполнять сопрограммы, возобновление которых запланировано на 1 секунду позже.
Этот тест полностью детерминирован, то есть он всегда будет выполняться одинаково. И, поскольку он полностью контролирует выполнение сопрограмм, запущенных в Dispatchers.Main
, ему не нужно ждать ни секунды для установки значения.
Запустить существующий тест
- Щелкните правой кнопкой мыши по имени класса
MainViewModelTest
в редакторе, чтобы открыть контекстное меню. - В контекстном меню выберите
Запустить «MainViewModelTest»
- Для будущих запусков вы можете выбрать эту тестовую конфигурацию в конфигурациях рядом с
кнопку на панели инструментов. По умолчанию конфигурация будет называться MainViewModelTest .
Вы должны увидеть, как тест пройдёт! И его выполнение займёт чуть меньше секунды.
В следующем упражнении вы узнаете, как преобразовать существующие API обратного вызова для использования сопрограмм.
На этом этапе мы начнём конвертировать репозиторий для использования сопрограмм. Для этого мы добавим сопрограммы в ViewModel
, Repository
, Room
и Retrofit
.
Полезно понимать, за что отвечает каждая часть архитектуры, прежде чем переключаться на использование сопрограмм.
-
MainDatabase
реализует базу данных, использующую Room, которая сохраняет и загружаетTitle
. -
MainNetwork
реализует сетевой API, который извлекает новые видео. Для извлечения видео используется Retrofit.Retrofit
настроен на случайный возврат ошибок или ложных данных, но в остальном ведёт себя так, как будто выполняет настоящие сетевые запросы. -
TitleRepository
реализует единый API для извлечения или обновления заголовка путем объединения данных из сети и базы данных. -
MainViewModel
представляет состояние экрана и обрабатывает события. Он сообщает репозиторию о необходимости обновления заголовка при касании экрана пользователем.
Поскольку сетевой запрос управляется событиями пользовательского интерфейса и мы хотим запустить сопрограмму на их основе, естественным местом для начала использования сопрограмм является 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
с помощью сопрограмм!
Поскольку она нам понадобится прямо сейчас, давайте создадим пустую функцию suspend в нашем репозитории ( 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 мы можем убедиться, что счетчик всегда выключен после выполнения запроса.
Запустите приложение еще раз, выбрав начальную конфигурацию и нажав , вы увидите индикатор загрузки при нажатии в любом месте. Заголовок останется прежним, поскольку мы ещё не подключили сеть и базу данных.
В следующем упражнении вы обновите репозиторий, чтобы он действительно работал.
В этом упражнении вы узнаете, как переключить поток, в котором работает сопрограмма, чтобы реализовать рабочую версию 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
реализован с обратным вызовом для передачи вызывающему объекту состояния загрузки и ошибки.
Эта функция делает довольно много вещей для реализации обновления.
- Переключиться на другой поток с помощью
BACKGROUND
ExecutorService
- Выполнить сетевой запрос
fetchNextTitle
, используя блокирующий методexecute()
. Это запустит сетевой запрос в текущем потоке, в данном случае в одном из потоков вBACKGROUND
. - Если результат успешен, сохраните его в базе данных с помощью
insertTitle
и вызовите методonCompleted()
. - Если результат не удался или возникло исключение, вызовите метод onError, чтобы сообщить вызывающему объекту о неудачном обновлении.
Эта реализация, основанная на обратных вызовах, безопасна для основного потока, поскольку не блокирует его. Однако она должна использовать обратный вызов, чтобы информировать вызывающую сторону о завершении работы. Она также вызывает обратные вызовы в BACKGROUND
потоке, на который она переключилась.
Вызов блокирования вызовов из сопрограмм
Не добавляя сопрограммы в сеть или базу данных, мы можем сделать этот код безопасным для основного выполнения, используя сопрограммы. Это позволит нам избавиться от обратного вызова и передать результат обратно потоку, который изначально его вызвал.
Вы можете использовать этот шаблон в любое время, когда вам нужно выполнить блокирующую или ресурсоемкую работу процессора из сопрограммы, например, сортировку и фильтрацию большого списка или чтение с диска.
Для переключения между любыми диспетчерами сопрограммы используют withContext
. Вызов withContext
переключает на другой диспетчер только для лямбда-выражения, а затем возвращает вызвавшему его диспетчеру результат этой лямбда-выражения.
По умолчанию сопрограммы Kotlin предоставляют три диспетчера: Main
, IO
и Default
. Диспетчер 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
.
По сравнению с версией обратного вызова есть два важных отличия:
-
withContext
возвращает результат обратно вызвавшему его Dispatcher, в данном случаеDispatchers.Main
. Версия обратного вызова вызывала обратные вызовы в потоке в службе-исполнителеBACKGROUND
. - Вызывающему не нужно передавать обратный вызов этой функции. Он может использовать функции suspend и resume для получения результата или ошибки.
Запустите приложение еще раз.
Если вы снова запустите приложение, вы увидите, что новая реализация на основе сопрограмм загружает результаты из сети!
На следующем этапе вы интегрируете сопрограммы в 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
Теперь давайте посмотрим, как интегрировать сопрограммы с 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, вам необходимо сделать две вещи:
- Добавьте модификатор приостановки к функции
- Удалите обёртку
Call
из возвращаемого типа. Здесь мы возвращаемString
, но можно вернуть и сложный тип на основе JSON. Если вы всё равно хотите предоставить доступ к полномуResult
Retrofit, вы можете вернутьResult<String>
вместоString
из функции suspend.
Retrofit автоматически сделает функции приостановки main-safe, так что вы сможете вызывать их напрямую из 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)
}
}
Ого, это намного короче. Что случилось? Оказывается, использование suspend и resume позволяет сделать код гораздо короче. Retrofit позволяет нам использовать здесь возвращаемые типы данных, такие как String
или объект User
, вместо Call
. Это безопасно, потому что внутри функции suspend Retrofit
может выполнить сетевой запрос в фоновом потоке и возобновить сопрограмму после завершения вызова.
Более того, мы избавились от withContext
. Поскольку и Room, и Retrofit предоставляют функции приостановки , безопасные для основного процесса , можно безопасно организовать эту асинхронную работу из Dispatchers.Main
.
Исправление ошибок компилятора
Переход на сопрограммы подразумевает изменение сигнатуры функций, поскольку невозможно вызвать функцию suspend из обычной функции. При добавлении модификатора suspend
на этом этапе было сгенерировано несколько ошибок компиляции, которые показывают, что произойдёт, если изменить функцию на suspend в реальном проекте.
Просмотрите проект и исправьте ошибки компиляции, изменив функцию на suspend created. Вот краткие решения для каждой из них:
TestingFakes.kt
Обновите тестовые фейки для поддержки новых модификаторов приостановки.
TitleDaoFake
- Нажмите Alt-Enter, чтобы добавить модификаторы приостановки ко всем функциям в иерархии.
MainNetworkFake
- Нажмите Alt-Enter, чтобы добавить модификаторы приостановки ко всем функциям в иерархии.
- Замените
fetchNextTitle
этой функцией
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Нажмите Alt-Enter, чтобы добавить модификаторы приостановки ко всем функциям в иерархии.
- Замените
fetchNextTitle
этой функцией
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Удалите функцию
refreshTitleWithCallbacks
, так как она больше не используется.
Запустите приложение
Запустите приложение еще раз, как только оно компилирует, вы увидите, что он загружает данные, используя Coroutines на протяжении всего пути от ViewModel в комнату и модернизацию!
Поздравляю, вы полностью поменяли это приложение на использование Coroutines! Чтобы завершить, мы немного поговорим о том, как проверить, что мы только что сделали.
В этом упражнении вы напишите тест, который напрямую вызывает функцию suspend
.
Поскольку refreshTitle
выставлено как публичный API, он будет протестирован напрямую, показывая, как вызовать функции Coroutines из тестов.
Вот функция refreshTitle
, которую вы реализовали в последнем упражнении:
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)
}
}
Напишите тест, который вызывает функцию приостановки
Open TitleRepositoryTest.kt
в test
папке, в которой есть два Todos.
Попробуйте позвонить в refreshTitle
с первого теста whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Поскольку refreshTitle
является suspend
функцией, Kotlin не знает, как ее назвать, кроме как из -за коратики или другой функции подвески, и вы получите ошибку компилятора, например, «Обновление функции приостановки следует вызвать только из коратики или другой функции подвески».
Тестовый бегун ничего не знает о Coroutines, поэтому мы не можем сделать этот тест функцией приостановки. Мы могли бы launch
Coroutine, используя CoroutineScope
, как в ViewModel
, однако тесты должны запускать коратики до завершения до их возвращения. Как только функция тестирования вернется, тест закончился. Coroutines, начатые с launch
, являются асинхронным кодом, который может завершить в какой -то момент в будущем. Поэтому, чтобы проверить этот асинхронный код, вам нужен какой -нибудь способ сказать, что тест подождать, пока ваша коратика не завершится. Поскольку launch
является неблокирующим вызовом, это означает, что он возвращается сразу же и может продолжать запускать корутину после возврата функции - ее нельзя использовать в тестах. Например:
@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
}
Этот тест иногда терпит неудачу. Вызов для launch
вернется немедленно и выполнит в то же время, что и остальная часть тестового примера. Тест не имеет возможности узнать, заработал ли refreshTitle
еще или нет - и любые утверждения, такие как проверка того, что база данных была обновлена. И, если refreshTitle
бросила исключение, его не будет брошен в стек тестовых вызовов. Вместо этого он будет добавлен в непредучанный обработчик исключений GlobalScope
.
Библиотека kotlinx-coroutines-test
имеет функцию runBlockingTest
, которая блокирует, пока она вызывает функции приостановки. Когда runBlockingTest
вызывает функцию приостановки или launches
новую Coroutine, она выполняет ее сразу по умолчанию. Вы можете думать об этом как о способе преобразования функций подвески и супругов в обычные вызовы функций.
Кроме того, runBlockingTest
будет пересматривать неопределенные исключения для вас. Это облегчает тестирование, когда коратика бросает исключение.
Реализовать тест с одной коратикой
Оберните вызов, чтобы refreshTitle
с помощью runBlockingTest
и удалить GlobalScope.launch
.
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")
}
В этом тесте используются подделки, предоставленные, чтобы проверить, что «OK» вставляется в базу данных с помощью refreshTitle
.
Когда тестовые вызовы runBlockingTest
, он будет блокироваться до тех пор, пока не завершится runBlockingTest
. Затем, когда мы называем refreshTitle
, он использует регулярный механизм приостановки и резюме, чтобы дождаться добавления строки базы данных в нашу подделку.
После завершения теста Coroutine, runBlockingTest
возвращает.
Напишите тайм -аут
Мы хотим добавить короткий тайм -аут к запросу сети. Давайте сначала напишем тест, а затем внедрим тайм -аут. Создайте новый тест:
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)
}
В этом тесте используется предоставленная поддельная MainNetworkCompletableFake
, которая представляет собой подделку сети, которая предназначена для приостановки абонентов до тех пор, пока тест не продолжит их. Когда refreshTitle
попытается сделать сетевой запрос, он будет зависеть вечно, потому что мы хотим проверить тайм -ауты.
Затем он запускает отдельную корутину, чтобы вызвать refreshTitle
. Это ключевая часть тайм -аутов тестирования, тайм -аут должен произойти в другой коратике, чем тот, который создает runBlockingTest
. Таким образом, мы можем позвонить в следующую строку, advanceTimeBy(5_000)
, которая будет продвигаться на 5 секунд и привести к тому, что другая коратика будет время ожидания.
Это полный тест тайм -аута, и он пройдет после реализации тайм -аута.
Запустите это сейчас и посмотрите, что произойдет:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Одна из функций runBlockingTest
заключается в том, что он не позволит вам протекать Corubines после завершения теста. Если в конце теста есть какие -либо незаконченные скручивания, такие как наш запуск Coroutine, он пройдет сбой.
Добавьте тайм -аут
Откройте TitleRepository
и добавьте в сеть пять секунд. Вы можете сделать это, используя функцию withTimeout
:
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)
}
}
Запустите тест. Когда вы запускаете тесты, вы должны увидеть все тесты!
В следующем упражнении вы узнаете, как писать функции более высокого порядка, используя Coroutines.
В этом упражнении вы Refactor refreshTitle
в MainViewModel
для использования общей функции загрузки данных. Это научит вас, как строить функции более высокого порядка, которые используют Coroutines.
Текущая реализация работы refreshTitle
, но мы можем создать общую корутину загрузки данных, которая всегда показывает спиннер. Это может быть полезно в кодовой базе, которая загружает данные в ответ на несколько событий, и хочет обеспечить последовательно отображение загрузки.
Просмотр текущей реализации Каждая строка, кроме repository.refreshTitle()
является шаблоном, чтобы показать ошибки спиннера и отображения.
// 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
}
}
}
Использование CORUTINES в функциях более высокого порядка
Добавьте этот код в 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
}
}
}
Теперь Refactor refreshTitle()
для использования этой функции более высокого порядка.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Аннотация логики, показывающая загружающую спиннер, и показывая ошибки, мы упростили наш фактический код, необходимый для загрузки данных. Показать прядильщик или отображение ошибки - это то, что легко обобщить на любую загрузку данных, в то время как фактический источник данных и пункт назначения необходимо определять каждый раз.
Чтобы построить эту абстракцию, launchDataLoad
берет block
аргументов, который является приостановкой лямбды. Приостановка лямбда позволяет вызову функций приостановки. Вот как Kotlin реализует launch
и runBlocking
Coroutine Builders, которые мы использовали в этом CodeLab.
// suspend lambda
block: suspend () -> Unit
Чтобы сделать приостановку лямбды, начните с ключевого слова suspend
. Функциональная стрелка и Unit
возврата типа завершают объявление.
Вам не часто должны объявлять свои собственные приостановки лямбды, но они могут быть полезны для создания подобных абстракций, которые инкапсулируют повторную логику!
В этом упражнении вы узнаете, как использовать код на основе Coroutine от Workmanager.
Что такое Workmanager
Есть много вариантов на Android для отсроковой фоновой работы. Это упражнение показывает вам, как интегрировать Workmanager с Coroutines. WorkManager - это совместимая, гибкая и простая библиотека для отсроковой фоновой работы. Workmanager является рекомендуемым решением для этих вариантов использования на Android.
Workmanager является частью Android JetPack и архитектурным компонентом для фоновой работы, которая требует комбинации оппортунистического и гарантированного исполнения. Оппортунистическое исполнение означает, что Workmanager выполнит вашу фоновую работу как можно скорее. Гарантированное исполнение означает, что Workmanager позаботится о логике, чтобы начать вашу работу в различных ситуациях, даже если вы уходите от своего приложения.
Из -за этого Workmanager - хороший выбор для задач, которые должны выполнить в конечном итоге.
Некоторые примеры задач, которые хорошо используют Workmanager:
- Загрузка журналов
- Применение фильтров на изображения и сохранение изображения
- Периодически синхронизировать локальные данные с сетью
Использование CORUTINES с Workmanager
Workmanager предоставляет различные реализации своего базового класса ListanableWorker
для различных вариантов использования.
Самый простой класс работников позволяет нам выполнять синхронную операцию, выполненную WorkManager. Однако, работая до сих пор, чтобы преобразовать нашу кодовую базу для использования CORUTINES и подвески функций, лучший способ использования WorkManager - через класс CoroutineWorker
, который позволяет определить нашу функцию doWork()
как функцию приостановки.
Чтобы начать, откройте RefreshMainDataWork
. Он уже расширяет CoroutineWorker
, и вам нужно внедрить doWork
.
Внутри функции suspend
doWork
вызовите refreshTitle()
из репозитория и верните соответствующий результат!
После того, как вы завершили Todo, код будет выглядеть так:
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()
}
}
Обратите внимание, что CoroutineWorker.doWork()
является подвесной функцией. В отличие от класса более простых Worker
, этот код не работает для исполнителя, указанного в вашей конфигурации Workmanager, а вместо этого используйте диспетчер в члене coroutineContext
(по умолчанию Dispatchers.Default
).
Тестирование нашего коратика
Никакая кодовая база не должна быть полной без тестирования.
Workmanager предоставляет пару различных способов протестировать ваши Worker
, чтобы узнать больше об оригинальной инфраструктуре тестирования, вы можете прочитать документацию .
Workmanager v2.1 представляет новый набор API, чтобы поддержать более простой способ протестировать классы ListenableWorker
и, как следствие, CoroutineWorker. В нашем коде мы собираемся использовать один из этих новых API: TestListenableWorkerBuilder
.
Чтобы добавить наш новый тест, обновите файл RefreshMainDataWorkTest
в папке androidTest
.
Содержимое файла:
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())
}
}
Прежде чем мы пройдемся испытание, мы рассказываем WorkManager
о заводе, чтобы мы могли вводить поддельную сеть.
Сам тест использует TestListenableWorkerBuilder
для создания нашего работника, который мы можем затем запустить, вызывая метод startWork()
.
Workmanager - только один пример того, как Coroutines можно использовать для упрощения дизайна API.
В этом CodeLab мы рассмотрели основы, которые вам понадобятся, чтобы начать использовать Coroutines в вашем приложении!
Мы покрыли:
- Как интегрировать CORUTINES в приложения Android с заданий пользовательского интерфейса и Workmanager, чтобы упростить асинхронное программирование,
- Как использовать Coroutines внутри
ViewModel
для извлечения данных из сети и сохранить их в базу данных, не блокируя основной поток. - И как отменить все круги, когда
ViewModel
закончена.
Для тестирования кода на основе коратики мы рассмотрели как поведение тестирования, так и непосредственное вызов функций suspend
из тестов.
Узнать больше
Ознакомьтесь с « Advanced Coroutines с Kotlin Flow и Livedata » CodeLab, чтобы узнать больше об использовании Coroots на Android.
Коратики в котлин имеют много функций, которые не были покрыты этим кодирусом. Если вы заинтересованы в том, чтобы узнать больше о коратиках Kotlin, прочитайте руководства Coroutines, опубликованные Jetbrains. Также ознакомьтесь с « Улучшением производительности приложений с помощью коратиков Kotlin » для большего количества моделей использования Coroutines на Android.