Как использовать Котлин корутины (Kotlin Coroutines) в андроид приложении. Часть 1

Введение

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

В этом курсе вы узнаете, как использовать 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.
  • Лучшие практики для тестирования корутин.

Что нужно знать

  • Знакомство с компонентами архитектуры ViewModelLiveDataRepository, и Room.
  • Знакомство с синтаксисом Kotlin, включая функции расширения (extension) и лямбды.
  • Основное понимание использования потоков в Android, включая основной поток, фоновые потоки и обратные вызовы.

Для введения в Компоненты Архитектуры, см. Room with a View.

Для ознакомления с синтаксисом Kotlin см. Kotlin Bootcamp for Programmers.

Для ознакомления с основами многопоточности в Android см. Guide to background processing.



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

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

Установка Android Studio

Приступаем к настройке

Исходный код

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

… или клонируйте GitHub-репозиторий из командной строки, используя следующую команду:

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

Репозиторий kotlin-coroutines содержит три разных приложения:

  • android_studio_folder.pngkotlin-coroutines-start — Простое приложение, чтобы изучить, как сделать свою первую корутину
  • android_studio_folder.pngkotlin-coroutines-repository — Проект основан на обратных вызовах, которые вы конвертируете для использования корутин.
  • android_studio_folder.pngkotlin-coroutines-end — Проект с корутинами уже добавлен

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

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

  1. Если вы загрузили zip-файл kotlin-coroutines, распакуйте его.
  2. Откройте проект kotlin-coroutines-start в Android Studio.
  3. Нажмите кнопку  execute.pngRun и выберите эмулятор или подключите устройство Android, которое должно поддерживать Android Lollipop (минимальный поддерживаемый SDK – 21). Экран Kotlin Coroutines должен появиться:

Как использовать Котлин корутины (Kotlin Coroutines) в андроид приложении. Часть 1

Это стартовое приложение использует потоки, чтобы отобразить снэк-бар через секунду после нажатия в любом месте экрана. Попробуйте сейчас, и вы должны увидеть “Hello, from threads!”  после небольшой задержки В первой части этого курса вы конвертируете это приложение для использования корутин.

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

  1. MainActivity отображает пользовательский интерфейс, регистрирует слушатели кнопок и может отображать Snackbar. Он передает события MainViewModel и обновляет экран на основе LiveData в MainViewModel.
  2. MainViewModel обрабатывает события в onMainViewClicked и будет общаться с MainActivity используя LiveData.
  3. Executors определяет BACKGROUND, который может запускать работу в фоновом потоке.
  4. 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. Управление пользовательским интерфейсом

Коментарі: 9
  1. Riotto
    Riotto

    Учебный проект, используемый в курсе, не собирается… Как исправить?

    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)

    1. Виталий Непочатов
      admin (автор)

      Попробуйте удалить папку .idea в корне проекта и повторно импортировать проект в Android Studio

    2. Riotto
      Riotto

      Удалил, ничего не изменилось. И на другом компе то же самое.

    3. Виталий Непочатов
      admin (автор)

      Какой из проектов не запускается? Их несколько в архиве, импортируйте их по отдельности.

    4. Riotto
      Riotto

      Возможно, я что-то не так делаю в процессе? Или проблема в более новой версии студии, чем при написании тестового проекта? Вряд ли же у меня одного такая сложность возникла. При экспорте с гитхаба такая же ошибка. Буду признателен, если поможете разобраться.

    5. Виталий Непочатов
      admin (автор)

      У вас какая версия Android Studio? Используйте 3.6.1, на ней все работает, проверил только что.

    6. Riotto
      Riotto

      Разобрался, всё работает, спасибо за помощь!

  2. Mikhail Seliverstov
    Rinkirikakita

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

    “По сравнению с кодом, основанным на колбеках, код на корутинах выполняет тот же результат, что и разблокирование текущего потока, но с меньшим количеством кода.” – не могу понять, как код корутин может выполнять результат, что и разблокирование текущего потока … Для меня это набор слов, если не затруднит, можно перефразировать для танкистов?))) Буду очень благодарен!

    1. Виталий Непочатов
      admin (автор)

      Трудности перевода. Заменили формулировку на следующую: “По сравнению с кодом, основанным на колбеках, код на корутинах в процессе работы также не блокирует текущий поток, но с меньшим количеством кода.”

Додати коментар