null

Немного про Coroutines в Kotlin

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

CoroutineContext

Каждая корутина выполняется в каком-то контексте. Этот контекст представляется классом CoroutineContext. Контекст напоминает собой типизированную мапу, у которой каждый ключ представлен элементом типа key и этот тип знает, какое должно вернуться значение, при получении по нему значения из контекста. Эти значения из контекста реализуют интерфейс Element, который расширяет интерфейс CoroutineContext. Фактически, получается, что каждое значение из контекста тоже является контекстом. 


Создавать явно контекст вам не придется. Так как обычно он задается либо в scope, либо определяется при запуске корутины. 
Но также можно объединить несколько контекстов в один или удалить элементы из них.
Все стандартные корутин-билдеры принимают на вход контекст. Но он не используется, а лишь происходит его сложение с CoroutineContext из scope, в котором будет запускаться корутина.

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

Первый компонент контекста - это job. Он представляет собой выполняемую фоне задачу. Например объект job вы получите при использовании CoroutineBuilder launch{}, как результат создания корутины. 

Запуск корутины с помощью async, возвращает уже объект типа defert, который расширяет интерфейс job.

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

Жизненный цикл job состоит из шести состояний. При создании job она находится в состоянии new. После старта переходит в состояние active. Если корутина завершается успешно, то она переходит состояние completing и ожидает завершения дочерней job, а затем переходит состоянии complete. Если происходит отмена или возникает ошибка, то сначала мы получим состоянии cancelling, а затем уже cancelled. 

Каждая job может иметь родительскую job’у, но это необязательно. Иерархическая организация позволяет связывать между собой корутины. Такая организация job’ов между собой, имеет также несколько важных особенностей. Например отмена job приведет к рекурсивной отмены всех ее дочерних job. Так же это работает и в обратном порядке. В случае возникновения ошибки или отмены это приводит к отмене всех родителей они рекурсивно отменят всех своих дочерней job. Чтобы такого поведения происходило и отменялись только родители вам нужно использовать специальную реализацию job - SupervisorJob (принципе он является основным).

Интерфейс job довольно простой и содержит всего лишь несколько методов.
abstract fun cancel(cause: CancellationException? = null)
cancel который отменяет выполнение job и принимает опциональную причину отмены, в виде CancellationException.

abstract fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
invokeOnCompletion позволяет вам задать callback, который будет выполнен по окончанию job.

abstract suspend fun join()
 join приостанавливают выполнение корутины и ожидает выполнение job.


abstract fun start(): Boolean
start запускает корутину, которая связана с этой job, если она ещё не была запущена.

Свойство children - это возможность получения всех дочерней job 

И функция ensureActive() проверяет активна ли текущая job, если нет, то выбросит CancellationException. 

Один из самых популярных элементов контекста - это coroutineDispatcher, который отвечает за то, на каком потоке или потоках будет выполняться соответствующая ему корутина. Фактически, он решает на каком потоке выполнить корутину, какой пул потоков использовать для ее выполнения или вовсе не менять поток и продолжить выполнение кода в том же потоке, в котором она запускалась.
Стандартные dispatcher и можно найти в классе dispatcher и они следующие: Default, Main, Unconfinded, IO.

CoroutineScope

Любая асинхронная операция, которая запускается в вашем приложении, должна быть остановлена, когда результаты выполнения больше вам не нужны. Это позволит не занимать лишние ресурсы и позволит работать устройству быстрее. Например, можно отменить получение данных, если пользователь уходит с экрана где они должны показаться, потому что больше в них нужды нет.  В потоках для этого вы использовали бы thread interrupt. Hо их проблема, это неудобство api. Вам нужно сохранять ссылки на объекты, через которые происходит отмена, причем это опционально и api вас не заставляет делать это обязательно. В корутинах же для этого сделали CoroutineScope. Он является неким жизненным циклом и ответственным за все дочерние корутины в рамках него, а также время их работы. Все запускающийся корутины должны быть привязаны к кому-то CoroutineScope. Именно поэтому все CoroutineBuilders являются расширениями типа CoroutineScope. Исключением является лишь runBlocking() которой Scope не нужен. Это и логично ведь этот специальный CoroutineBuilder предназначен для блокировки потока с которого запускается корутина, пока она не будет закончена. Это такой некий мост между блокирующим подходом и подходом основном на прерывании. Используйте runBlocking() только для запуска тестов или в main() функций, в других случаях это может привести к плохим последствиям и нарушить работу ваших приложений.

Во время работы с корутинами, вы будете сталкиваться с таким понятием как Structured Concurrency. Structured Concurrency - это механизм, который предоставляет иерархическую структуру для организации работы корутин. Фактически все принципы Structured Concurrency строятся на основе CoroutineScope. А под капотом через отношения родитель ребенок у job.
Принцип работы у Structured Concurrency следующие:
Scope хранит все ссылки на корутины, запущенные в рамках него. Scope может отменить выполнение всех дочерней корутин, если возникнет ошибка или операция будет отменена. Scope автоматически ожидает выполнение всех дочерних корутин в рамках него. Например, если Scope привязан корутине, то она не выполнится пока все корутины в рамках этого Scope не выполняться. Неважно успешно или с ошибкой.

CoroutineScope - это простейший интерфейс, содержащий единственное свойство CoroutineContext. Тогда разберемся, в чем тогда отличие CoroutineScope от CoroutineContext, ведь CoroutineScope - просто обертка. Целевое назначение использование и является этим отличием.  
CoroutineContext - является набором параметров для выполнения корутины, который обязательно есть у любой из них. CoroutineScope хоть и является оберткой над CoroutineContext, но предназначен для объединения всех запущенных корутин в рамках него. Также под капотом передает им job, которая будет их объединять и будет являться родительской для всех запущенных корутин в Scope. Также Scope является источником Element в контексте для построения корутин по умолчанию. Например, так можно передать дефолтный dispatcher для всех корутин или создать свой.

Первый Scope, который вы можете начать использовать - это GlobalScope. Oн не привязан какой-либо job, поэтому отменить его невозможно.  Корутины, запущенные в рамках него, будут выполняться пока не умрет процесс. Использование GlobalScope может легко привести к утечке памяти и нарушает принципы Structured Concurrency. Это api даже специально было помечено аннотацией @DelicateCoroutinesApi, чтобы при попытке его использования разработчик был предупреждён об опасности. Настоятельно не рекомендуется использовать GlobalScope или создавать собственный  Scope, который будет привязан к жизни вашего приложения. Например для android вы можете создать Scope, который будет привязан жизни application. Это позволит вам управлять таким жизненным циклом не нарушая принципов Structured Concurrency. Помимо этого, вы сможете добавить туда все необходимые вам элементы контекста и явно контролировать поведение самого главного жизненного цикла для корутин. 


Чтобы создать CoroutineScope, вы можете использовать одноименную функцию в нее вы можете передать различные элементы CoroutineContext. Любой CoroutineScope обязательно должен иметь в себе объект типа job. Если вы не укажите его явно, то при создании Scope он будет добавлен. Обычно вместо job используется SuperVizorJob? так как в этом случае ошибки из любой дочерней корутины не приведут к остановке всех корутин в Scope. Dispatcher задавать в CoroutineScope не обязательно, так как будет использован вариант по умолчанию (dispatcher.DEFAULT). 

Создать новый CoroutineScope, также можно с помощью вызова suspend функции coroutineScope(). Обычно она используется, когда нужно в рамках suspend функций запустить корутину. Например, чтобы выполнить задачу параллельно. 
При создании нового scope функция coroutineScope() возьмет контекст из родителя добавит к нему job, который будет связан с внешним scope. Полученный новый scope работает по следующим правилам: 
Если происходит ошибка внутри, то он будет пробрасываться наверх. 
Если внешний scope будет остановлен, то и этот scope будет также остановлен 
Scope будет завершен успешно, только тогда, когда выполнится весь код и все дочерние корутины внутри него.
 
Если вы хотите поведение аналогично CoroutineScope, но только не получать каскадную остановку корутин, то используйте функцию supervisorScope(). Она является полным аналогом coroutineScope(), но только вместо job будет использоваться SupervisorJob. Такие CoroutineScope не подойдут для того, чтобы выполнять операцию дольше, чем родительский scope. Например, пользователь нажимает кнопку “отправить сообщение” и закрывает экран. Логично, что сообщение должно дойти независимо от того показывается UI на экране или нет. Это может понадобиться, чтобы создавать отдельный CoroutineScope на уровне бизнес-логики. Важно, чтобы он не имел связи scope'ом UI и работал независимо.

Каждая корутина внутри себя передает CoroutineScope, что позволяет вызывать другие корутины внутри нее, без указания scope. Так как явно функция вызывается в нужном контексте. Этот scope формируется по следующим правилам: берется CoroutineContext из scope, в котором запускается корутина. К нему добавляются все элементы из контекста, которые вы задали в параметрах при запуске корутины. И при получении job из scope внутри корутины, вы будете получать текущую корутину. То есть job тоже создается новый и связывается. Это позволяет организовать иерархию корутин и запуска их на основе job.

Любой CoroutineScope является активным самого его старта. Чтобы уничтожить его,  надо явно вызвать функцию cancel(), которая явно остановит все запущенные корутины в рамках него. При попытке запустить любую корутину после остановки scope, корутина будет сразу останавливаться с ошибкой. Если вам нужно остановить все картины в рамках scope, но scope должен остаться живым. Для этого нужно получить CoroutineContext, затем объект job из нее, и вот на него уже вызвать cancel(), которая остановит все дочерние корутины.

Как уже было сказано, CoroutineScope построен поверх CoroutineContext и связь между корутинами строится через job. Так что порой, для каких-то низкоуровневых операций вам придется достучаться до контекста, в котором лежит CoroutineScope и достать оттуда что-то и провести какую-то операцию. 

Если в рамках выполнения корутины, вам надо сменить контекст для части кода внутри. То для этого не обязательно запускать новую корутину, а лучше использовать функцию withContext(). Например, это используется для того, чтобы сменить dispatcher для выполнения блока кода на нем. Рекомендуется, всегда явно использовать dispatcher внутри suspend функции,  чтобы она выполнялась на правильном. Например, если у вас какая-то загрузка файлов и вам нужен dispatcher IO, то явно в начале функции укажите это.