Kotlin android корутины пример

Корутины

Введение в корутины

В последнее время поддержка асинхронности и параллельных вычислений стала неотъемлимой чертой многих языков программирования. И Kotlin не является исключением. Зачем нужны асинхронность и параллельные вычисления? Параллельные вычисления позволяют выполнять несколько задач одновременно, а асинхронность позволяет не блокировать основной ход приложения во время выполнения задачи, которая занимает продолжительное время. Например, мы создаем графическое приложение для десктопа или мобильного устройства. И нам надо по нажатию на кнопку отправлять запрос к интернет-ресурсу. Однако подобный запрос может занять довольно много время. И чтобы приложение не зависало на период отправки запроса, подобные запросы к интернет-ресурсам следует отправлять асинхронно. При асинхронных запросах пользователь не ждет пока придет ответ от интернет-ресурса, а продолжает работу с приложением, а при получении ответа получит соответствующее уведомление.

В языке Kotlin поддержка асинхронности и параллельных вычислений воплощена в виде корутин ( coroutine ). По сути корутина представляет блок кода, который может выполняться параллельно с остальным кодом. А базовая функциональность, связанная с корутинами, сосредоточена в библиотеке kotlinx.coroutines .

Рассмотрим определение и применение корутины на простейшем примере.

Добавление kotlinx.coroutines

Прежде всего стоит отметить, что функциональность корутин (библиотека kotlinx.coroutines ) по умолчанию не включена в проект. И нам ее надо добавить. Если мы создаем проект консольного приложения в IntelliJ IDEA, то мы можем добавить соответствующую библиотеку в проект. Для этого в меню File перейдем к пункту Project Structure..

Далее на вкладке «Project Settings» перейдем к пункту Libraries . В центральном поле отобразятся библиотеки, добавленные в проект. И для добавления новой библиотеки нажмем на знак плюса:

Нас перебросит к папке lib , которая содержит устанавливаемые по умолчанию библиотеки Kotlin. Выберем среди них библиотеку kotlinx-coroutines-core-jvm.jar и нажмем на OK для ее добавления:

После добавления в проекте в узле External Libraries / KotlinJavaRuntime мы увидим добавленную библиотеку:

В других типах проектов для подключения kotlinx.coroutines может использоваться Gradle или другие способы. Так, если проект использует Gradle, то в файл gradle добавляется зависимость:

Определение suspend-функции

Сначала рассмотрим пример, который не использует корутины:

Здесь в функции main перебираем последовательность от 0 до 5 и выводит текущий элемент последовательности на консоль. Для имитации продолжительной работы внутри цикла вызываем специальную функцию delay() из пакета kotlinx.coroutines . В эту функцию передается количество миллисекунд, на которое выполняется задержка. Передаваемое значение должно иметь тип Long. То есть здесь функция будет выполнять задержку в 400 миллисекунд перед выводом на консоль текущего элемента последовательности.

После выполнения работы цикла выводим на консоль строку «Hello Coroutines».

И чтобы использовать внутри функции main функцию delay() , функция main предваряется модификатром suspend . Модификатор suspend определяет функцию, которая может приостановить свое выполнение и возобновить его через некоторый период времени.

Сама функция delay() то же является подобной функцией, которая определена с модификатором suspend . А любая функция с модификатором suspend может вызывать только либо из другой функции, которая тоже имеет модификатор suspend , либо из корутины.

Если мы запустим приложение, то мы увидим следующий консольный вывод:

Здесь мы видим, что строка «Hello Coroutines» выводится после выполнения цикла. Но вместо цикла у нас могла бы быть более содержательная, но и более продолжительная работа, например, обращение к интернет-ресурсу, к удаленой базе данных, какие-то операции с файлами и т.д. И в этом случае все определенные после этой работы действия ожидали бы завершения этой продолжительной работы, как в данном случае строка «Hello Coroutines» ждет завершения цикла.

Определение корутины

Теперь вынесем продолжительную работу — то есть цикл в корутину:

Прежде всего для определения и выполнения корутины нам надо определить для нее контекст, так как корутина может вызываться только в контексте корутины (coroutine scope). Для этого применяется функция coroutineScope() — создает контекст корутины. Кроме того, эта функция ожидает выполнения всех определенных внутри нее корутин. Стоит отметить, что coroutineScope() может применяться только в функции с модификатором, коей является функция main.

Сама корутина определяется и запускается с помощью построителя корутин — функции launch . Она создает корутину в виде блока кода — в данном случае это:

и запускет эту корутину параллельно с остальным кодом. То есть данная корутина выполняется независимо от прочего кода, определенного в функции main.

В итоге при выполнении программы мы увидим несколько другой консольный вывод:

Теперь строка «Hello Coroutines» не ожидает, пока завершится цикл, а выполняется параллельно с ним.

Вынесение кода корутин в отдельную функцию

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

В данном случае основной код корутины вынесен в функцию doWork() . Поскольку в этой функции применяется функция delay() , то doWork() определена с модификатором suspend . Сама корутина создается также с помощью функции launch() , которая вызывает функцию doWork() .

Корутины и потоки

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

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

Читайте также:  Adguard для yandex browser android

Источник

Корутины Kotlin: как работать асинхронно в Android

Kotlin предоставляет корутины, которые помогают писать асинхронный код синхронно. Android — это однопоточная платформа, и по умолчанию все работает на основном потоке (потоке UI). Когда настает время запускать операции, несвязанные с UI (например, сетевой вызов, работа с БД, I/O операции или прием задачи в любой момент), мы распределяем задачи по различным потокам и, если нужно, передаем результат обратно в поток UI.

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

С корутиной код выглядит легче. Нам не нужно использовать обратный вызов, и следующая строка будет выполнена, как только придет ответ. Можно подумать, что вызов функции из основного потока заблокирует её к тому времени, как ответ вернется, но с корутиной все иначе. Она не будет блокировать поток Main или любой другой, но все еще может выполнять код синхронно. Подробнее.

Сравним корутины с потоком

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

1. Передача данных из одного потока в другой — это головная боль. Так еще и грязная. Нам постоянно нужно использовать обратные вызовы или какой-нибудь механизм уведомления.

2. Потоки стоят дорого. Их создание и остановка обходится дорого, включает в себя создание собственного стека. Потоки управляются ОС. Планировщик потоков добавляет дополнительную нагрузку.

3. Потоки блокируются. Если вы выполняете такую простую задачу, как задержка выполнения на секунду (Sleep), поток будет заблокирован и не может быть использован для другой операции.

4. Потоки не знают о жизненном цикле. Они не знают о компонентах Lifecycle (Activity, Fragment, ViewModel). Поток будет работать, даже если компонент UI будет уничтожен, что требует от нас разобраться с очисткой и утечкой памяти.

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

Отметим, что это не просто другой способ асинхронного программирования , это другая парадигма.

Корутины легкие и супербыстрые

Пусть код скажет за себя.

Я создам 10к потоков, что вообще нереалистично, но для понимания эффекта корутин пример очень наглядный:

Здесь каждый поток ожидает 1 мс. Запуск этой функции занял около 12,6 секунд. Теперь давайте создадим 100к корутин (в 10 раз больше) и увеличим задержку до 10 секунд (в 10000 раз больше). Не волнуйтесь про “runBlocking” или “launch” (конструкторах Coroutine).

14 секунд. Сама задержка составляет 10 секунд. Это очень быстро. Создание 100 тысяч потоков может занять целую вечность.

Если вы посмотрите на метод creating_10k_Thread(), то увидите, что существует задержка в 1 мс. Во время нее он заблокирован, т.е. ничего не может делать. Вы можете создать только определенное количество потоков в зависимости от количества ядер. Допустим, возможно создать до 8 потоков в системе. В примере мы запускаем цикл на 10000 раз. Первые 8 раз будут созданы 8 потоков, который будут работать параллельно. На 9-й итерации следующий поток не сможет быть создан, пока не будет доступного. На 1 мс поток блокируется. Затем создастся новый поток и по итогу блокировка на 1мс создает задержку. Общее время блокировки для метода составит 10000/ мс. А также будет использоваться планировщик потоков, что добавит дополнительной нагрузки.

Для creatingCoroutines() мы установили задержку в 10 сек. Корутина приостанавливается, не блокируется. Пока метод ждет 10 секунд до завершения, он может взять задачу и выполнить ее после задержки. Корутины управляются пользователем, а не ОС, что делает их быстрее. В цифрах, каждый поток имеет свой собственный стек, обычно размером 1 Мбайт. 64 Кбайт — это наименьший объем пространства стека, разрешенный для каждого потока в JVM, в то время как простая корутина в Kotlin занимает всего несколько десятков байт heap памяти.

Еще пример для лучшего понимания:

Во фрагменте 1 мы последовательно вызываем методы fun1 и fun2 в основном потоке. На 1 секунду поток будет заблокирован. Теперь рассмотрим пример с корутиной.

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

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

Как же корутина приостанавливает свою работу?

Если вы посмотрите на выход, то увидите, что ‘completionHandler’ выполняется после завершения ‘asyncOperation’. ‘asyncOperation’ выполняется в фоновом потоке, а ‘completionHandler’ ожидает его завершения. В ‘completionHandler’ происходит обновление textview. Давайте рассмотрим байтовый код метода ‘asyncOperation’.

Во второй строке есть новый параметр под названием ‘continuation’, добавленный к методу asyncOperation. Continuation (продолжение) — это рабочий вариант для приостановки кода. Продолжение добавляется в качестве параметра к функции, если она имеет модификатор ‘suspend’. Также он сохраняет текущее состояние программы. Думайте о нем как о передаче остальной части кода (в данном случае метода completionHandler()) внутрь оболочки Continuation. После завершения текущей задачи выполнится блок продолжения. Поэтому каждый раз, когда вы создаете функцию suspend, вы добавляете в нее параметр продолжения, который обертывает остальную часть кода из той же корутины.

Читайте также:  Как работает maps android

Источник

Урок 1. Корутины. Введение.

В этом уроке начнем разбираться, что такое корутина и suspend функция.

Сложная тема

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

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

В защиту авторов документации я должен сказать, что тема действительно очень сложна для объяснения. Для меня корутины по сложности легко обошли такие непростые темы, как Dagger или Backpressure в RxJava. Я потратил кучу времени, чтобы разобраться, что же такое корутины и как они работают. Читал официальные доки, делал примеры, читал статьи, смотрел видео. И все равно у меня оставалось ощущение, что я не понимаю их до конца сам, а значит не могу объяснить другим. Пришлось прибегнуть к последнему, самому надежному и самому сложному средству — лезть в исходники. Врагу не пожелаю туда соваться, но в итоге я таки пробился через эти дебри и постепенно пазл собрался.

В этом курсе я собираюсь достаточно подробно осветить тему корутин, используя для этого все то, что мне удалось раскопать. Я буду шаг за шагом расписывать отдельные кусочки пазла и периодически собирать эти кусочки в большие куски, объясняя очередную тему. В итоге у вас должна сложиться общая картина. Иногда может показаться, что я слишком ухожу в дебри объяснений, и эта информация не нужна вовсе, и давайте лучше сразу с боевых примеров начнем! Но тут я исхожу из своего опыта. Мне удалось полностью понять примеры только после того, как я раскопал внутренности. И сейчас моя цель — сделать так, чтобы вы, посмотрев на код с корутинами, могли точно сказать, как он себя поведет. А для этого нужно понять корутины изнутри.

Поэтому первые несколько уроков будут состоять только из объяснений. Я подробно расскажу о том, во что превращается корутина при преобразовании Kotlin кода в Java. А также о том, почему suspend функция не блокирует поток. Об этом будут первые 5 уроков. А уже после этого пойдут более интересные и приближенные к практике темы: Scope, Context, Job и т.п.

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

Краткий вариант уроков

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

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

Краткий вариант подходит для тех, кто уже хоть немного, но знаком с темой. А также тех, кто ранее уже прочел подробный вариант и зашел просто освежить знания или подсмотреть какой-то момент. Тут не будет подробных объяснений и долгого хождения вокруг темы. Все лаконично и тезисно.

Выбирайте вариант, который подходит вам. Можете сразу попробовать краткий вариант для экономии времени. Если видите, что информации недостаточно, и нужно более вдумчивое объяснение, то заваривайте кружку чая, берите плед и открывайте подробный вариант)

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

Корутина

Непросто подобрать описание, что такое корутина. В самом начале обычно рассматривается такой простой пример:

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

Здесь launch — это билдер корутины, которому передается небольшой блок кода:

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

Если очень упрощать, то можно сказать, что билдер — это экзекьютор, который оборачивает блок кода в Runnable и запускает этот Runnable на выполнение. Т.е. корутину можно представить так:

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

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

Это дает нам некоторые возможности по управлению корутиной. Мы можем сделать выполнение кода отложенным (LAZY) и стартовать его позже, когда понадобится. Или в любой момент времени можно будет отменить выполнение. Также можно запускать корутину внутри корутины. Их джобы будут связаны между собой отношениями Parent-Child, что является отдельным механизмом, который влияет на обработку ошибок и отмену корутин. Все это мы еще подробно рассмотрим в дальнейших уроках.

А пока что нас интересует одна из самых важных особенностей корутин — suspend функции.

Suspend функция

Чтобы лучше понять, что это такое, поговорим про потоки. Поток выполнения корутины — это вовсе не обязательно фоновый поток. Им вполне может быть и main поток. В Android мы часто будем запускать корутины в main потоке. И, как мы все знаем, этот поток нельзя блокировать.

Читайте также:  Медиаплееры для андроид обзор

В примере выше мы в корутине используем suspend функцию delay. Эта функция приостановит выполнение кода на 1000 мсек, затем выполнение кода продолжится и напишет в лог слово World. Вроде ничего не обычного. Но обратите внимание, я написал «приостановит выполнение кода», а не «заблокирует поток».

В этом и есть ключевая особенность suspend функции. Она не блочит поток. А значит мы можем эту корутину запускать в main потоке. Функция delay приостановит выполнение кода на 1 секунду, но не заблокирует main поток.

Давайте рассмотрим пример, более приближенный к реальности. Например, загрузка файла.

Код без всяких корутин и suspend функций будет таким:

Мы генерируем URL файла и передаем его в функцию download, которая выполнит загрузку. После этого мы выводим Toast-сообщение о том, что файл загружен.

Функция download — синхронная, а значит заблокирует поток, в котором будет выполнена. Но нам хотелось бы выполнить этот код в UI потоке, потому что функция toast может быть выполнена только в нем. Чтобы разрешить это противоречие, нам придется сделать функцию download асинхронной и toast поместить в колбэк, который будет выполнен по завершении загрузки.

Т.е. при запуске тяжелого кода в main потоке нам приходится выбирать из двух зол: блокировка потока или колбэк. Если функция download синхронна — то поток будет заблокирован. А если она асинхронна, то нам придется добавлять колбэк, чтобы выполнить последующий код.

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

Перепишем код с использованием корутины и suspend функции:

Эта корутина не заблокирует поток, в котором будет запущен ее код. Т.е. его можно запустить даже в main потоке. Функция download загрузит файл в отдельном потоке, а toast будет выполнен только после того, как download отработает.

Это кажется магией, но не забывайте, что Kotlin код будет преобразован в Java классы. И во время этих преобразований будет использован механизм Continuation. Он в сочетании с suspend функциями реализует колбэк, который обеспечит выполнение toast только после того, как загрузка файла будет завершена.

О том, как именно это происходит, подробно поговорим в следующем уроке.

Корутина

В самом начале документации и статей обычно рассматривается такой простой пример:

Здесь launch — это билдер корутины, которому передается блок кода. Билдер упакует переданный ему блок кода в корутину и запустит ее.

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

Это дает нам некоторые возможности по управлению корутиной. Например, мы можем сделать выполнение кода отложенным (lazy) и стартовать его позже, когда понадобится. Или в любой момент времени можно будет отменить выполнение. Также можно запускать корутину внутри корутины. Их джобы будут связаны между собой отношениями Parent-Child, что является отдельным механизмом, который влияет на обработку ошибок и отмену корутин.

Suspend функция

В примере выше мы в корутине используем suspend функцию delay. Эта функция приостановит выполнение кода на 1000 мсек, затем выполнение кода продолжится и напишет в лог слово World. При этом delay не заблокирует поток. В этом и есть ключевая особенность suspend функций. А значит мы можем эту корутину запускать в main потоке.

Давайте рассмотрим пример, более приближенный к реальности. Например, загрузка файла.

Код без всяких корутин и suspend функций будет таким:

Мы генерируем URL файла и передаем его в функцию download, которая выполнит загрузку. После этого мы выводим Toast-сообщение о том, что файл загружен.

Функция download — синхронная, а значит заблокирует поток, в котором будет выполнена. Но нам хотелось бы выполнить этот код в UI потоке, потому что функция toast может быть выполнена только в нем. Чтобы разрешить это противоречие, нам придется сделать функцию download асинхронной и toast поместить в колбэк, который будет выполнен по завершении загрузки.

Использование suspend функций в корутинах позволяют нам выполнять долгие асинхронные функции без колбэков:

Эта корутина не заблокирует поток, в котором она будет запущена. А toast будет выполнен только после того, как download отработает. Это достигается с помощью механизма Continuation, который используется при преобразовании Kotlin кода в Java классы.

О том, как именно это происходит, подробно поговорим в следующем уроке.

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Kotlin, RxJava, Dagger, Тестирование

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Комментарии

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

Спасибо автору за этот курс. Тема выбрана просто идеально, ибо я считаю корутины самой сложной частью котлина (еще конечно обрезки от дженериков, но то таке) и давно хотел с этим разобраться. Рад что заскочил сюда и увидел что есть такой курс.

ЗЫ Я довольно долго и упорно гуглил чтобы понять как работают потроха корутин, но нигде нет инфы! Курс — вышка, рекомендую. Человек потратил время, разобрался, расшарил — красавчик

Источник

Оцените статью