Лучший опыт

Корутины: основы.

Часть 1, Часть 2 Эта серия постов подробно посвящена отменам и исключениям в корутинах. Отмена важна тем, что она помогает избежать выполнения большего количества работы, которое в свою очередь может привести к потере памяти и времени автономной работы. Правильная обработка исключений является ключом к отличному пользовательскому опыту. В качестве основы для других 2 частей серии (часть 2: отмена, часть 3: исключения) важно определить
Корутины: основы...

Часть 1, Часть 2

Эта серия постов подробно посвящена отменам и исключениям в корутинах. Отмена важна тем, что она помогает избежать выполнения большего количества работы, которое в свою очередь может привести к потере памяти и времени автономной работы. Правильная обработка исключений является ключом к отличному пользовательскому опыту. В качестве основы для других 2 частей серии (часть 2: отмена, часть 3: исключения) важно определить некоторые основные понятия, связанные с корутиной, такие как CoroutineScope, Jobи CoroutineContext, чтобы мы находились на одной волне.

Если вы предпочитаете видео, посмотрите этот разговор из KotlinConf’19 от Флорины Мунтенеску и от меня:

CoroutineScope

CoroutineScopeотслеживает любую корутину, созданную с помощью launchили async (это функции расширения в CoroutineScope). Текущая работа (запущенные корутины) может быть отменена вызовом scope.cancel() в любой момент времени.

Вам необходимо создавать CoroutineScope всякий раз, когда вы хотите запустить и контролировать жизненный цикл сопрограмм в определенном слое вашего приложения. В некоторых платформах, таких как Android, существуют библиотеки KTX, которые уже предоставляют CoroutineScope в определенных классах жизненного цикла, например viewModelScope и lifecycleScope.

При создании CoroutineScope принимает CoroutineContext в качестве параметра для своего конструктора. Вы можете создать новую область и корутину, используя следующий код:

// Job и Dispatcher комбинируются в CoroutineContext, который // опишу чуть позже val scope = CoroutineScope(Job() + Dispatchers.Main)val job = scope.launch {     // новая корутина }

Job

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

CoroutineContext

CoroutineContext— это набор элементов, определяющих поведение корутины. Он состоит из:

  • Job — управляет жизненным циклом корутины.
  • CoroutineDispatcher — отправляет работу в соответствующий поток.
  • CoroutineName — имя корутины, полезно для отладки.
  • CoroutineExceptionHandler — обрабатывает неотловленные исключения, которые будут рассмотрены в 3 части серии о корутинах.

Что такое CoroutineContext новой корутины? Мы уже знаем, что будет создан новый экземпляр Job, позволяющий нам контролировать жизненный цикл корутины. Остальные элементы будут унаследованы от CoroutineContext её родителя (либо другой корутины или CoroutineScope, где была создана корутина).

Поскольку CoroutineScope может создавать корутины, а вы можете создавать дополнительные корутины внутри корутины, создается неявная иерархия задач. В следующем фрагменте кода написано, как, помимо создания новой корутины с помощью CoroutineScope, можно создать дополнительные корутины внутри корутины:

val scope = CoroutineScope(Job() + Dispatchers.Main)val job = scope.launch {     // Родительский элемент новой корутины - CoroutineScope     val result = async {         // New coroutine that has the coroutine started by          // launch as a parent     }.await() }

Корнем этой иерархии обычно является CoroutineScope. Мы могли бы представить себе эту иерархию следующим образом:

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

Жизненный цикл Job

Job может проходить через множество состояний: новое, активное, завершение, завершенное, отмена и отмененное. Хотя у нас нет доступа к самим состояниям, мы можем получить доступ к свойствам Job: isActive, isCancelled и isCompleted.

Жизненный цикл Job

Если корутина находится в активном состоянии, то происходит либо сбой, либо вызов job.cancel() переведет Job в состояние Отмены (isActive = false, isCancelled = true). Как только все дети Jobзавершат свою работу, корутина перейдет в состояние Отмененное и isCompleted = true.

Объяснение родительского CoroutineContext

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

Родительский контекст = значения по умолчанию + унаследованный CoroutineContext + аргументы

Где:

  • Некоторые элементы имеют значения по умолчанию: Dispatchers.Default — значение по умолчанию CoroutineDispatcher и “coroutine” по умолчанию для CoroutineName.
  • Унаследованный CoroutineContext— это CoroutineContext созданного им CoroutineScope или корутины.
  • Аргументы, передаваемые в конструктор корутины, будут иметь приоритет над этими элементами в наследуемом контексте.

Примечание: CoroutineContext можно комбинировать с помощью оператора +. Поскольку CoroutineContext— это набор элементов, будет создан новый CoroutineContext с элементами в правой части от плюса, переопределяющими те, что находятся слева. Например: (Dispatchers.Main, “name”) + (Dispatchers.IO) = (Dispatchers.IO, “name”)

Каждая корутина, запущенная этой CoroutineScope, будет иметь в CoroutineContext по крайней мере эти элементы . CoroutineName закрашено серым, потому что оно является значением по умолчанию.

Теперь, когда мы знаем, что такое родительский CoroutineContext новой корутины, его фактический CoroutineContext будет:

Новый контекст корутины = родительский CoroutineContext + Job()

С помощью CoroutineScope, показанного на рисунке выше, мы создадим новую корутину:

val job = scope.launch(Dispatchers.IO) {  // новая корутина }

Что такое родительский CoroutineContext этой корутины и её фактический CoroutineContext? Смотрите решение на рисунке ниже!

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

Результирующий родительский CoroutineContext имеет Dispatchers.IO вместо области видимости CoroutineDispatcher, так как он был переопределен аргументом конструктора корутины. Кроме того, нужно проверить, что Job в родительском CoroutineContext является экземпляром области Job (красный цвет), а новый экземпляр Job(зеленый цвет) был присвоен CoroutineContext новой корутины.

После части 3 этой серии станет ясно, что CoroutineScope может иметь другую реализацию Job под названием SupervisorJob в своем CoroutineContext, которая изменяет то, как CoroutineScope работает с исключениями. Таким образом, новая корутина, созданная с вышеупомянутой CoroutineScope, может иметь SupervisorJob в качестве родительского Job. Однако, если родителем корутины является другая корутина, то родительское задание всегда будет иметь тип Job.