Введение
Это первый курок курса о том, как использовать Котлин корутины в андроид приложении.
В этом курсе вы узнаете, как использовать Kotlin Coroutines в приложении для Android — новый способ управления фоновыми потоками (background threads), который может упростить код за счет уменьшения потребности в обратных вызовах (callbacks). Корутины, или сопрограммы — это функция Kotlin, которая преобразует асинхронные обратные вызовы для длительных задач, таких как доступ к базе данных или сети, в последовательный (sequential) код.
Чтобы на практике увидеть работу с Kotlin Coroutines и архитектурными компонентами, записывайтесь на продвинутый курс по разработке приложения «Чат-мессенжер»
Вот фрагмент кода, чтобы дать вам представление о том, что вы будете делать:
// 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
Вы начнете с существующего приложения, созданного с использованием компонентов архитектуры, которое использует стиль обратного вызова для длительных задач.
К концу этого курса у вас будет достаточно опыта для преобразования существующего API для использования корутин, и вы сможете интегрировать корутины в приложение. Вы также познакомитесь с лучшими практиками для корутин и с тем, как написать тест для кода, который использует корутины.
Что вы узнаете
- Как вызвать код, написанный с корутинами и получить результаты.
- Как использовать функции приостановки (suspend), чтобы сделать асинхронный код последовательным.
- Как использовать launch и runBlocking для контроля выполнения кода.
- Методы преобразования существующих API в корутины с использованием suspendCoroutine.
- Как использовать корутины с Architecture Components.
- Лучшие практики для тестирования корутин.
Что нужно знать
- Знакомство с компонентами архитектуры
ViewModel
,LiveData
,Repository
, иRoom
. - Знакомство с синтаксисом Kotlin, включая функции расширения (extension) и лямбды.
- Основное понимание использования потоков в Android, включая основной поток, фоновые потоки и обратные вызовы.
Для введения в Компоненты Архитектуры, см. Room with a View.
Для ознакомления с синтаксисом Kotlin см. Kotlin Bootcamp for Programmers.
Для ознакомления с основами многопоточности в Android см. Guide to background processing.
Что вам понадобится
Android Studio 3.3 (можно работать с другими версиями, но некоторые вещи могут отсутствовать или выглядеть иначе).
Приступаем к настройке
Исходный код
Нажмите на кнопку, чтобы загрузить весь код для серии уроков:
… или клонируйте GitHub-репозиторий из командной строки, используя следующую команду:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Репозиторий kotlin-coroutines содержит три разных приложения:
- kotlin-coroutines-start — Простое приложение, чтобы изучить, как сделать свою первую корутину
- kotlin-coroutines-repository — Проект основан на обратных вызовах, которые вы конвертируете для использования корутин.
- kotlin-coroutines-end — Проект с корутинами уже добавлен
Запуск приложения
Во-первых, давайте посмотрим, как выглядит исходный пример приложения. Следуйте этим инструкциям, чтобы открыть образец приложения в Android Studio.
- Если вы загрузили zip-файл kotlin-coroutines, распакуйте его.
- Откройте проект kotlin-coroutines-start в Android Studio.
- Нажмите кнопку Run и выберите эмулятор или подключите устройство Android, которое должно поддерживать Android Lollipop (минимальный поддерживаемый SDK — 21). Экран Kotlin Coroutines должен появиться:
Это стартовое приложение использует потоки, чтобы отобразить снэк-бар через секунду после нажатия в любом месте экрана. Попробуйте сейчас, и вы должны увидеть «Hello, from threads!» после небольшой задержки В первой части этого курса вы конвертируете это приложение для использования корутин.
Это приложение использует компоненты архитектуры для отделения кода пользовательского интерфейса в MainActivity от логики приложения в MainViewModel. Найдите минутку, чтобы ознакомиться со структурой проекта.
MainActivity
отображает пользовательский интерфейс, регистрирует слушатели кнопок и может отображатьSnackbar
. Он передает событияMainViewModel
и обновляет экран на основеLiveData
вMainViewModel
.MainViewModel
обрабатывает события вonMainViewClicked
и будет общаться сMainActivity
используяLiveData.
Executors
определяетBACKGROUND,
который может запускать работу в фоновом потоке.MainViewModelTest
определяет тест дляMainViewModel
.
Добавление корутин в проект
Чтобы использовать корутины в 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" }
Корутины и RxJava
Если вы используете RxJava в своем проекте, вы можете использовать корутины с RxJava с помощью библиотеки kotlin-coroutines-rx.
Корутины в Котлине
На Android важно избегать блокировки основного потока. Основной поток — это отдельный поток, который обрабатывает все обновления пользовательского интерфейса. Это также поток, который вызывает все обработчики кликов и другие обратные вызовы пользовательского интерфейса. Как таковой, он должен работать бесперебойно, чтобы гарантировать отличный пользовательский опыт.
Чтобы ваше приложение отображалось пользователю без видимых пауз, основной поток должен обновлять экран каждые 16 мс или чаще, что составляет около 60 кадров в секунду. Многие обычные задачи занимают больше времени, например, анализ больших наборов данных JSON, запись данных в базу данных или выборка данных из сети. Таким образом, вызов подобного кода из основного потока может привести к приостановке, заиканию или даже зависанию приложения. И если вы заблокируете основной поток слишком долго, приложение может даже аварийно завершить работу и отобразить диалоговое окно «Приложение не отвечает».
Шаблон обратного вызова (callback pattern)
Одним из шаблонов выполнения долгосрочных задач без блокировки основного потока являются обратные вызовы. Используя обратные вызовы, вы можете запускать длительные задачи в фоновом потоке. Когда задача завершается, вызывается обратный вызов, чтобы сообщить вам о результате в главном потоке.
Взгляните на пример шаблона обратного вызова:
// 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 — это способ Котлина обозначить функцию или тип функции, доступные для корутин. Когда корутина вызывает функцию, помеченную как 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 работает своим волшебством.
Важно: Ключевое слово 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 { ... }
Корутины под другим именем
Шаблон async-
await
на других языках основан на сопрограммах. Если вы знакомы с этим шаблоном, ключевое слово suspend
похоже на async. Однако в Котлине, await()
подразумевается при вызове suspend
функций.
В Котлине есть метод Deferred.await()
который используется для ожидания результата от сопрограммы, начатой с async
builder.
В следующем уроке вы преобразуете стартовый пример приложения для использования корутин.
Продолжение:
Котлин корутины. Часть 2. Управление пользовательским интерфейсом
Учебный проект, используемый в курсе, не собирается… Как исправить?
Error:Internal error: (java.lang.ClassNotFoundException) com.google.wireless.android.sdk.stats.IntellijIndexingStats$Index
java.lang.ClassNotFoundException: com.google.wireless.android.sdk.stats.IntellijIndexingStats$Index
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at com.intellij.util.indexing.counters.IndexCounters.(IndexCounters.java:34)
at com.intellij.util.indexing.impl.MapReduceIndex.(MapReduceIndex.java:94)
at com.intellij.util.indexing.impl.MapReduceIndex.(MapReduceIndex.java:110)
at org.jetbrains.jps.backwardRefs.index.CompilerReferenceIndex$CompilerMapReduceIndex.(CompilerReferenceIndex.java:248)
at org.jetbrains.jps.backwardRefs.index.CompilerReferenceIndex.(CompilerReferenceIndex.java:84)
at org.jetbrains.jps.backwardRefs.JavaCompilerBackwardReferenceIndex.(JavaCompilerBackwardReferenceIndex.java:12)
at org.jetbrains.jps.backwardRefs.JavaBackwardReferenceIndexWriter.initialize(JavaBackwardReferenceIndexWriter.java:80)
at org.jetbrains.jps.incremental.java.JavaBuilder.buildStarted(JavaBuilder.java:149)
at org.jetbrains.jps.incremental.IncProjectBuilder.runBuild(IncProjectBuilder.java:359)
at org.jetbrains.jps.incremental.IncProjectBuilder.build(IncProjectBuilder.java:178)
at org.jetbrains.jps.cmdline.BuildRunner.runBuild(BuildRunner.java:139)
at org.jetbrains.jps.cmdline.BuildSession.runBuild(BuildSession.java:288)
at org.jetbrains.jps.cmdline.BuildSession.run(BuildSession.java:121)
at org.jetbrains.jps.cmdline.BuildMain$MyMessageHandler.lambda$channelRead0$0(BuildMain.java:228)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Попробуйте удалить папку .idea в корне проекта и повторно импортировать проект в Android Studio
Удалил, ничего не изменилось. И на другом компе то же самое.
Какой из проектов не запускается? Их несколько в архиве, импортируйте их по отдельности.
Возможно, я что-то не так делаю в процессе? Или проблема в более новой версии студии, чем при написании тестового проекта? Вряд ли же у меня одного такая сложность возникла. При экспорте с гитхаба такая же ошибка. Буду признателен, если поможете разобраться.
У вас какая версия Android Studio? Используйте 3.6.1, на ней все работает, проверил только что.
Разобрался, всё работает, спасибо за помощь!
Спасибо большое за инфу, но есть некоторые моменты, которые меня убивают, как говорится либо лыжи не едут, либо … В общем вопрос в этой фразе:
«По сравнению с кодом, основанным на колбеках, код на корутинах выполняет тот же результат, что и разблокирование текущего потока, но с меньшим количеством кода.» — не могу понять, как код корутин может выполнять результат, что и разблокирование текущего потока … Для меня это набор слов, если не затруднит, можно перефразировать для танкистов?))) Буду очень благодарен!
Трудности перевода. Заменили формулировку на следующую: «По сравнению с кодом, основанным на колбеках, код на корутинах в процессе работы также не блокирует текущий поток, но с меньшим количеством кода.»