CategoryAndroid

Что такое Flow

Flow — это упакованный в объект фрагмент кода, который производит какой-то последовательный набор элементов (данных). С одной стороны есть условный Отправитель, который этот код создал и готов предоставить его в виде Flow обертки. А с другой стороны есть Получатель, который возьмет этот Flow, запустит его и предоставит ему специальный колбэк, куда Flow будет складывать данные, который он производит.

Если вы знаете RxJava, то Flow — это аналог Observable/Flowable. Т.е. это некая фабрика, которая умеет производить данные. При создании она ничего не делает. Но как только мы ее запустим, она начнет работу и будет постить результаты в колбэк, который мы ей предоставим.

Flows are cold

Вычисления данных для Flow обычно холодные (cold) — Flow, созданный билдером flow {…}, является пассивной сущностью. Рассмотрим следующий код:

val coldFlow = flow {
    while (isActive) {
        emit(nextEvent)
    }
}

Сами Flow не начинают вычисляться и не хранят состояния пока на них не подпишется collector. Каждая корутина с collector-ом создает новый экземпляр кода, упаковывающего данные во Flow.

Но что насчет таких событий, как действия пользователя, события из операционной системы от датчиков устройства или о изменении состояния? Они появляются независимо от того, есть ли сейчас какой-либо collector, который в них потенциально заинтересован. Они также должны поддерживать нескольких collectors внутри приложения. Это так называемые горячие источники данных…

Shared flows
Вот здесь-то и появляется концепция SharedFlow. SharedFlow существует независимо от того, есть-ли сейчас collectors или нет. Collector у SharedFlow называется подписчиком (observer). Все observers получают одинаковую последовательность значений. Он работает как BroadcastChannel, но эффективнее и делает концепцию BroadcastChannel устаревшей.

SharedFlow — это легковесная широковещательный event bus, который вы можете создать и использовать в своей архитектуре приложения.
Все observers SharedFlow асинхронно собирают данные в своем собственном coroutine context. Emmiter не ждет, пока подписчики закончат обработку данных. Однако, когда общий буфер SharedFlow заполнен, emmiter приостанавливается, пока в буфере не появится место. Альтернативные стратегии работы с переполненным буфером настраиваются параметром BufferOverlow.

В SharedFlow события транслируются неизвестному количеству (⩾0) подписчиков. При отсутствии подписчика любое опубликованное событие немедленно удаляется. Это шаблон проектирования можно использовать для событий, которые должны обрабатываться немедленно или не обрабатываться вообще.

State flows
Частый способ справиться с переполнением буфера — отбрасывать старые данные и сохранять только новые. В частности, при единичном размере буфера мы имеем дело со state variable. Это настолько распространенный вариант использования, что у него есть собственный специализированный тип — StateFlow.

Смотрите на StateFlow как на изменяемую(мутабельную) переменную, на изменения которой можно подписаться. Его последнее значение всегда доступно, и, фактически, последнее значение — единственное, что важно для observers.

Обработка ошибок в Корутинах

Обработка ошибок в Корутинах всегда вызывала затруднения у многих разработчиков по ряду причин. Мало того, что существует несколько способов их обработки, но в первую очередь необходимо понять механизм распространения ошибок, который идет со структурированным параллелизмом(structered concurrency).

И есть множество ситуаций, когда распространение ошибок с помощью Корутин происходит по-разному, например:

  • Ошибки, возникающие в Scope-ах с CoroutineExceptionHandler или без него (родительские и дочерние Scope-ы)
  • Ошибки, возникающие в блоке try-catch
  • Ошибки во вложенных Корутинах
  • Ошибки, возникающие в SupervisorJob

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

ЕСЛИ корневая Job относится к scope-у с CoroutineExceptionHandler, ошибка будет обработана соответствующим образом, в противном случае просто будет выброшено исключение, и ваше приложение, вероятно, выйдет крашнется.

Это означает, что наличие CoroutineExceptionHandler где угодно, кроме корня, абсолютно ничего не делает.

Так почему же Корутины действуют таким образом? Почему сбой одного потомка останавливает всю сопрограмму вплоть до корня?

Почему исключения доходят до корня?

Ну, суть в том, что одна Корутина предназначена для достижения одного результата. Если кто-то из потомков не смог выполнить свое задание, было бы напрасной тратой ресурсов позволить закончить задание другим потомкам. Давайте рассмотрим пример.

getChannelsPageInfo — это корневая Корутина, которой требуется информация от getUser(), getChannels() и getSchedule() для отображения в виде страницы каналов. Все три из этих запросов являются обязательными для страницы для отображения необходимой информации.

Если getSchedule() потерпит неудачу, нет необходимости тратить больше ресурсов на другие сопрограммы. Страница не может правильно отобразить желаемое состояние, поэтому вместо этого она возвращается в состояние ошибки.

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

Затем ошибка распространяется до корня, getChannelsPageInfo(), и именно корень должен обработать ошибку. Вот где CoroutineExceptionHandler полезен.

private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    displayErrorState(throwable)
}
private fun getChannelsPageInfo() = viewModelScope.launch(exceptionHandler) {
    val user = async { getUser() }
    val channels = async { getChannels() }
    val schedule = async { getSchedule() }
    displayPageInfo(user.await(), channels.await(), schedule.await())
}

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

Правило 1
«Если корневая Корутина должна обрабатывать ошибки так же, как и в случае сбоя любого из ее дочерних элементов, следует использовать CoroutineExceptionHandler»

Но что, если мне нужно особое поведение при обработке ошибок в каждой из моих дочерних Корутин?

Использование блоков try-catch

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

private suspend fun getChannels(): List<Channels> =
    try {
        tvRepository.getChannels()
    } catch (exception: Exception) {
        emptyList()
    }

Теперь каждая из наших дочерних корутин будет выглядеть примерно так. Если tvRepository.getChannels() генерирует исключение, оно будет обработано в блоке try-catch. Исключение не вызывается самой Корутиной, поэтому никакие Корутины-братья не отменяются.

Правило 2
«Если исключение в дочерней Корутине не должно останавливать другие Корутины, принадлежащие тому же родителю, или требуется особое поведение при обработке ошибок, следует использовать блоки try-catch».

Вложенные Корутины внутри блоков try-catch

val user = async {
    try {
        async {
           	getUser()
        }.await()
    } catch (exception: Exception) {
        getBlankUser()
    }
}

В приведенном выше примере блок try-catch совершенно бесполезен. Если getUser() вызовет исключение, исключение будет передано в корень, и вся сопрограмма будет отменена. Это действительно немного странно. Наверняка вы ожидаете, что все, что находится внутри try, будет обрабатываться методом catch. Но не все так просто=) В мире корутин все не так, как кажется.

Когда Корутина внутри блока try-catch генерирует исключение и отменяется, исключение не генерируется самой функцией Корутины, то есть оно не генерируется async-ом. Вместо этого исключение переносится объектом Job, связанным с Корутиной, и распространяется на его родителей. Насколько известно блоку try-catch, ошибки не было.

Почему этого не произошло в нашем предыдущем блоке try-catch? Посмотрите еще раз.

private suspend fun getChannels(): List<Channels> = try {
    tvRepository.getChannels()
} catch (exception: Exception) {
    emptyList()
}

Внутри блока try-catch заключена не сама Корутина. Там нет ни launch, ни async, а только содержимое Корутины. Нет объекта Job, связанного напрямую с tvRepository.getChannels(). Если возникнет исключение, он повторно выдаст исключение, и блок try-catch обнаружит его.

Правило 3

«Если Корутина, запущенная внутри блоков try-catch, не работает, она не будет перехвачена блоком try-catch и продолжит распространение своей ошибки в корень»

CoroutineScope

private fun getChannelsPageInfo() = viewModelScope.launch(exceptionHandler) {
    val user = async { getUser() }
    val channels = try {
        coroutineScope {
            async {
                val channelOne = getChannelOne()
                val channelTwo = getChannelTwo()
                combineChannels(channelOne, channelTwo)
            }
        }
    } catch (exception: Exception) {
        async { createEmptyChannelInfo() }
    }
    val schedule = async { getSchedule() }
    displayPageInfo(user.await(), channels.await(), schedule.await())
}

Когда вы заключаете Корутину в coroutineScope, исключения, созданные в пределах scope-а, будут повторно брошена самой coroutineScope а не перебрасываться до корня. Это означает, что вы можете обернуть его в блок try-catch, не отменяя родительскую Корутину. Если вы действительно хотели вложить Корутины в блоки try-catch, то можно делать так.

Правило 4

«CoroutineScope можно использовать для повторной генерации исключений, создаваемых дочерними Корутинами, содержащимися внутри него»

SupervisorScope
Использование SupervisorScope почти похоже на запуск дочерней Корутины. Это превращает getChannels() в SupervisorJob, и если какой-либо из его дочерних элементов выйдет из строя, ошибка будет распространена на getChannels(), но не более того. Затем getChannels() должен обработать его либо с помощью собственного CoroutineExceptionHandler, либо с помощью результата Deferred.await().

private fun getChannelsPageInfo() = viewModelScope.launch(exceptionHandler) {
    val user = async { getUser() }
    val channels = supervisorScope {
        async {
            val channelOne = getChannelOne()
            val channelTwo = getChannelTwo()
            combineChannels(channelOne, channelTwo)
        }
    }
    val schedule = async { getSchedule() }
    val channelInfo = try {
        channels.await()
    } catch (exception: Exception) {
        createEmptyChannelInfo()
    }
    displayPageInfo(user.await(), channelInfo, schedule.await())
}

Возьмем, к примеру, приведенный выше код. Корутины getChannels, являющейся SupervisorJob. Если бы getChannelOne() или getChannelTwo() потерпели неудачу, исключение теперь переходит channels.await(), и теперь мы можем обработать исключение.

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

supervisorScope {
    launch(channelsExceptionHandler) {
        val channelOne = getChannelOne()
        val channelTwo = getChannelTwo()
        combineChannelsAndPost(channelOne, channelTwo)
    }
}

Вот пример использования launch(). В этом случае лучше использовать CoroutineExceptionHandler. Если вы его не используете или не обрабатываете исключение, то генерируемое join(), ошибка будет распространяться до родителя как обычно.

Правило 5
«SupervisorScopes следует использовать, когда у вас есть дочерние Корутины, ошибки которых должны доходить только до определенной точки, а не до корня»

Заключение

  • В корутинной связке родительского и дочернего элементов исключения доходят на весь путь до корня.
  • Используйте CoroutineExceptionHandler, если вам нужен единственный способ обработки сбойных дочерних Корутин.
  • Используйте блоки try-catch внутри дочерних Корутин, когда вам нужна более конкретная обработка ошибок без необходимости отмены родительских и одноуровневых Корутин.
  • Корутины, запущенные внутри блоков try-catch, делают блоки try-catch бесполезными, поскольку исключение не генерируется самой сопрограммой, а вместо этого переносится на родительский блок.
  • CoroutineScope можно использовать для повторного запуска исключений Корутин, запущенных внутри него.
  • SupervisorScope можно использовать, когда дочерние элементы должны распространять ошибки только до определенного момента, а не до корня всей Корутины.

Как видите, лучшее решение для Корутин обработки ошибок — полностью ситуативное. Здесь нет единого ответа на все вопросы.

Dispatchers

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

Он не подходит для IO операций, но сгодится для интенсивных вычислений.

IO
Использует тот же пул потоков, что и диспетчер по умолчанию. Но его лимит на потоки равен 64 (или числу ядер процессора, если их больше 64).

Этот диспетчер подходит для выполнения IO операций (запросы в сеть, чтение с диска и т.п.).

Main
Main диспетчер запустит корутину в основном потоке. Не забываем что корутина не заблокирует поток а приостановит выполнение кода. В корутине, которая выполняется в Main потоке, мы можем спокойно писать обычный (типичный для Activity или фрагментов) код, который работает с UI. Но при этом мы можем в этом же коде вызывать suspend функции, которые будут асинхронно получать данные с сервера или БД. И нам не нужны будут колбэки и переключения потоков. Все это скрыто под капотом корутин, а наш код выглядит чище и лаконичнее.

Unconfined
У диспетчера Unconfined метод isDispatchNeeded возвращает false. Это приводит к тому, что при старте и возобновлении выполнения кода Continuation не происходит смены потока.

Т.е. при старте корутина выполняется в том потоке, где был вызван билдер, который эту корутину создал и запустил. А при возобновлении выполнения из suspend-функции, корутина выполняется в потоке, который использовался в suspend-функции для выполнения фоновой работы. Ведь именно в этом потоке мы вызываем continuation.resume.

Context

Для выполнения каждой Корутины нужен CoroutineContext. А если точнее CoroutineContext-ы). А если еще точнее то компоненты наследники от класса CoroutineContext. Это необходимые компоненты, которые нужны для контроля над корутиной, переключения потоков выполнения, обработки ошибок и т.д. Но где и куда его вставлять? =) В предыдущих примерах мы уже встраивали и использовали CoroutineContext. Помните Job-у, которую мы вписывали в контруктор scope-а? Ну так вот Job это производная от интерфейса CoroutineContext.

Производными от CoroutineContext являются также CoroutineDispatcher, CoroutineExceptionHandler.

CoroutineContext имеет «под капотом» реализацию своеобразной типизированной Map-ы. Он хранит в себе элементы(интерфейс CoroutineContext.Element), и их можно достать по ключу. Это означает, что мы можем комбинировать разные Job-ы, Dispatcher-ы в один объект и вставлять в scope! Давайте посмотрим как это сделать:

val myContext = Job() + Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable -> }
val myScope = CoroutineScope(myContext)

или

val myScope = CoroutineScope(Job() + Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable -> })

Если помните, в прошлых уроках мы создавали scope следующим образом:

private val scope = CoroutineScope(Job())

Вместо Context мы передавали Job. Это работает, т.к. любой элемент, который можно поместить в Context, сам по себе также является Context-ом. Т.е. Job — это просто Context с одним элементом.

Аналогично и Dispatchers.Default — это Context с одним элементом. Можно сделать так:

val scope = CoroutineScope(Dispatchers.Default)

Scope поймет, что ему передают Context с одним элементом — диспетчером.

А этот код трехэлментного CoroutineContext:

val myContext = Job() + Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable -> }

Job для scope будет создан автоматически, если его нет в контексте.
Когда мы создаем scope и передаем ему Context, выполняется проверка, что этот Context содержит Job. И если не содержит, то Job будет создан.

Передача данных Contextа при создании корутин
В уроке про scope мы выяснили, что каждая корутина сама по себе является scope. Это необходимо, чтобы создавать дочерние корутины. Если быть более конкретным, то Job корутины является по совместительству ее scope-ом, а следовательно и содержит контекст корутины. Давайте разберемся, как этот контекст наполняется элементами.

Когда билдер создает новую корутину, он создает для нее новый пустой Context и помещает туда элементы из контекста родителя этой корутины. Т.е. элементы контекста scope будут переданы в контекст корутины, созданной в этом scope. Аналогично элементы контекста родительской корутины будут переданы в контекст ее дочерней корутины.

Но есть пара нюансов.

Во-первых, Job не передается. Для создаваемой корутины создается новый Job, который подписывается на Job родителя и помещается в контекст созданной корутины.

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

Builders

Билдер launch()
Начнем с билдера launch. Мы уже использовали его в прошлых уроках, чтобы создавать корутины. Но у него есть еще пара возможностей, о которых надо рассказать отдельно.

В прошлом уроке мы рассматривали такой пример:

private fun onRun() {
   scope.launch {
       // ...
   }

   // ...
}

Мы выяснили, что билдер launch создает и запускает корутину. Билдер не ждет, пока корутина выполнится, и не блокирует выполнение метода onRun. Билдер быстро делает свое дело, метод onRun продолжает выполнять свой дальнейший код и запросто может завершиться раньше, чем корутина из него запущенная. Это стандартное поведение многопоточного кода.

Но иногда может возникнуть необходимость дождаться выполнения корутины. Для этого используется метод Job.join()

Схематично это выглядит так:

job = scope.launch {
   // ...
}

// ...

job.join()

Билдер launch создает и запускает корутину и возвращает ее job. Используя метод join мы приостанавливаем выполнение кода, пока корутина не завершит работу. Т.е. мы запускаем работу в новом потоке, а в своем потоке ждем, пока работа не завершится. Но тут есть нюанс. Обратите внимание, я сказал, что join приостанавливает, а не блокирует выполнение кода. Это suspend-функция. А значит ее надо вызывать внутри корутины.

scope.launch {

   job1 = launch {
       // ...
   }

   job2 = launch {
       // ...
   }

   job1.join()
   job2.join()
}

Вызываем launch внутри launch, тем самым создавая дочернюю корутину внутри родительской. В родительской корутине мы можем вызвать suspend-функцию join(), чтобы дождаться завершения работы дочерней корутины.

Теперь в точке вызова join родительская корутина будет ждать, пока не выполнится дочерняя. Напомню, что join — это suspend-функция, поэтому она только приостановит выполнение родительской корутины, но не заблокирует ее поток.

Билдер asynс()
Билдер async похож на launch. Он также создает и стартует корутину. Но если launch корутина делала свою работу и ничего не возвращала в ответ, то async корутина может вернуть результат своей работы.

Как вы понимаете, чтобы получить результат работы корутины, надо дождаться окончания ее работы. Это очень похоже на ранее рассмотренный нами метод join. В случае с async это будет метод await. Он не только дождется окончания работы корутины, но и вернет результат ее работы.

val deferred = scope.async() {
   // ...
   "async result"
}
val result = deferred.await()

Вместо Job мы получаем Deferred. Это наследник Job, поэтому имеет все те же методы. Но дополнительно у него есть методы для получения результата работы корутины. Один из них — метод await.

Все аналогично примеру с launch+join. Билдер async создает и запускает дочернюю корутину. Родительская корутина продолжает выполняться и останавливается на методе await. Теперь она в ожидании завершения дочерней корутины.

Дочерняя корутина выполняется в отдельном потоке. По ее завершению метод await возвращает результат ее работы (строка “async result”), и возобновляет выполнение кода родительской корутины.

Параллельная работа
С помощью async можно распараллеливать работу suspend функций.

private fun onRun() {
   scope.launch {
       log("parent coroutine, start")

       val data = async { getData() }
       val data2 = async { getData2() }

       log("parent coroutine, wait until children return result")
       val result = data.await() + data2.await()
       log("parent coroutine, children returned: $result")

       log("parent coroutine, end")
   }
}

Билдер withContext
withContext это coroutineScope с возможностью добавить/заменить элементы контекста.

Чаще всего используется для смены диспетчера:

aunch {
   // get data from network or database
   // ...

   withContext(Dispatchers.Main) {
       // show data on the screen
       // ...
   }

   // ...
}

В launch выполняется какой-либо тяжелый код для получения данных. Далее с помощью withContext переключаемся на Main-поток и отображаем данные на экране. После чего корутина продолжает выполняться в своем диспатчере.

Либо корутина может выполняться в Main-потоке, а с помощью withContext мы можем переключиться на IO-поток, чтобы сделать запрос на сервер или в БД, и получить результат обратно в Main-поток.

Билдер runBlocking
runBlocking запускает корутину, которая блокирует текущий поток, пока не завершит свою работу (и пока не дождется завершения дочерних корутин).

В Android разработке такая корутина нужна для написания unit тестов, чтобы удержать поток, в котором выполняется тест. Иначе тест просто не дождется завершения выполнения вызываемых корутин и завершится. Подробнее об этом мы еще поговорим в уроке про тестирование.

Если в runBlocking произойдет ошибка, то все его дочерние корутины поотменяются, а сам он выбросит исключение.

Также он может вернуть результат своей работы.

Scope

Scope можно в чем-то сравнить с Активити Андроида. Если весь ЮИ отрисовывается в рамках Активити и нигде больше, то также и с Корутинами. Их можно запустить в рамках Scope-а. Также и suspend-функции запускаются в рамках scope-а. По примеру борща, scope можно сравнить с тарелкой, в котором будет борщ=)

Scope который отменяется за собой отменяет дочерние Корутины.

myScope.launch {
    // coroutine code
}

Используя билдер Launch прямо из scope-а можно запускать корутины в множественном числе. А после, если нужно будет удалить, то применить метод:

myScope.cancel()

По сути билдеры корутин Launch и Await являются расширениями класса CoroutineScope. То есть вне scope-а корутина не запустится.

Давайте посмотрим, что такое scope «под капотом».

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Это интерфейс. Объект, реализующий этот интерфейс, содержит CoroutineContext (он же контекст). Мы пока не изучали, что это. Если вкратце — то это хранилище, типа Map, в котором могут храниться различные объекты. О контексте поговорим позже.

Обратите внимание на поле coroutineContext. По контракту контекст scope-а должен хранить в себе Jobscope-а. Job является производной от класса CoroutineContext. Это действительно важно и вот почему.

Когда мы создаем корутину, у нее есть Job. Но у Job-а корутины должен быть Job-родитель. Это реализация structured concurrency. Смысл в том, что если мы отменим Job-родитель, то автоматически отменится и Job дочерней корутины.

Возникает вопрос — где взять Job-родитель, чтобы создать корутину. Вот именно для этого и нужен scope. Он (в своем контексте) содержит Job. Этот Job будет являться родителем для Job-ов корутин, которые мы создаем, вызывая myScope.launch().

И когда мы вызываем scope.cancel(), мы отменяем Job-родитель, а это отменит все Job-ы корутин, созданных с помощью этого scope. Т.е. отменой scope, отменяем все его корутины. Это и есть основной смысл scope.

Откуда взять scope?
Scope может быть предоставлен некоторыми объектами с жизненным циклом. Если мы используем этот scope, чтобы запускать корутины, то все его корутины будут отменены, когда объект завершает свою работу.

Пример такого объекта — ViewModel из Architecture Components. У него есть свойство viewModelScope.

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
        }
    }
}

Корутины, запущенные в этом scope, будут отменены, когда завершится ViewModel.

Каждая корутина создает внутри себя свой scope, чтобы иметь возможность запускать дочерние корутины. Именно этот scope и доступен нам как this в блоке кода корутины.

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

SupervisorScope
Если coroutineScope принимает ошибки от своих дочерних корутин и просто не шлет их дальше в родительскую корутину, то supervisorScope даже не принимает ошибку от дочерних. Это приводит к отличиям в обработке ошибок.

Корутина 1_2, которая пытается передать ошибку наверх в ScopeCoroutine, получает отрицательный ответ и пытается обработать ошибку сама. Для этого она использует предоставленный ей CoroutineExceptionHandler. Если же его нет, то будет крэш, который не поймать никаким try-catch.

Поэтому имеет смысл использовать CoroutineExceptionHandler внутри supervisorScope. В этом случае ошибка попадет в этот обработчик и на этом все закончится. Функция supervisorScope не выбросит исключение и не отменит остальные корутины внутри себя (т.е. корутину 1_1 в примере выше).

Continuation. Не «Магия» корутин

А теперь давайте посмотрим на то как происходит не блокирование потока и отсутствие колбэков «под капотом». Если честно, то колбэки все же есть=) Не все так просто, но и не сложно! Корутины используют специальный механизм Continuation.

Continuation можно рассматривать как колбэк. Continuation — похож на огромную стейт-машину, где на каждый кусок Корутин-кода(Suspend-функции) есть свой Switch-Case.

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

launch {
	val url = "http://myurl.com/file.pdf"

	// long synchronous function
	suspendedGetFile(url)

	println("File is downloaded")
}

Давайте посмотрим на Java класс, который получится в результате преобразования Kotlin кода корутины.

class GeneratedContinuationClass extends SuspendLambda {

    int label;
    String url;

    void invokeSuspend() {
        switch (label) {
            case 0: {  // шаг 1-ый
                url = buildUrl();
                label = 1;

                suspendedGetFile(url, this);  // Обратите внимание на "this". Его не было в Kotlin версии
                return;
            }              // <= Примерно тут происходит водораздел нашего кода. До старта suspend-функции и после
            case 1: {  // шаг 2-ой
                println("File is downloaded");
                return;
            }
        }
    }
}

Заметьте, что наша корутина трансформировалась в отдельный класс. Обратите внимание на метод invokeSuspend(), переменную label и на то как она используется в switch-е. Как вы успели заметить, основной код нашей Корутины(buildUrl(),suspendedGetFile(), println()) находится в методе invokeSuspend(). Он вызывается при запуске Корутины.

Continuation нужен для того чтобы код расположенный после suspendedGetFile() — а это println() — был исполнен только после завершения supendedGetFile(). Для этого код делится switch-ем на две части и добавляется переменная label. Точка разделения кода на две части — это suspend-функция.

Основная задача Continuation — сделать так, чтобы код, расположенный после вызова функции download(), был выполнен только когда метод download() завершил работу. Для этого код делится switch-ем на две части. И добавляется переменная label. Точка разделения кода на две части — это suspend-функция. От значения переменной label зависит, какая из двух частей будет выполнена при вызове invokeSuspend(). В нашем примере invokeSuspend() будет вызван 2 раза. Ключевые слова «case» в данном случае стоит понимать как «шаги»

Итак, первый раз invokeSuspend() будет вызван при старте Корутины. Он выполнит блок кода в case0: поменяет значение label на 1 и запустит suspend-функцию. Второй раз invokeSuspend() будет вызван уже из нашей suspend-функции suspendedGetFile(), когда она завершит свою работу. Как так?=) Suspend-функция при преобразовании в Java получает дополнительный входной параметр с типом Continuation. Т.е. в suspend-функцию suspendedGetFile() мы передаем инстанс текущего Continuation-а — this.

Когда suspend-функция suspendedGetFile() закончит загрузку файла, она возьмет Continuation, который ей передали и вызовет его invokeSuspend() метод. label был установлен в 1 (еще до запуска suspend функции), поэтому switch пойдет во вторую ветку и метод toast будет выполнен.

Таким образом Continuation является колбэком для suspend-функции. Если в корутине есть несколько suspend-функций, то Continuation будет колбэком для всех них.

Возврат значения suspend-функцией
Suspend-функция может возвращать какое-либо значение, как результат своей работы. Давайте посмотрим, как эта ситуация обрабатывается в Continuation.

Немного обновим код:

launch {
	val url = "http://myurl.com/file.pdf"

	// long synchronous function
	val file = suspendedGetFile(url)

	println("File is downloaded")

	val size = suspendUnzip(file)

	println("Size is $size")
}

Мы добавили еще одну suspend-функцию suspendUnzip(). При трансформации кода в Java, код будет разделен на три части:
1) suspendedGetFile() и все, что перед ней
2) suspendUnzip() и все, что между ней и suspendedGetFile()
3) все, что после suspendUnzip()

Точки разделения — это suspend-функции.

File file;
Long size;

void invokeSuspend(Object result) {
    switch (label) {
        case 0: {
            url = buildUrl();
            label = 1;

            suspendedGetFile(url, this);
            return;
        }
        case 1: {
            file = (File) result;
            println("File is downloaded");
            label = 2;

            suspendUnzip(file, this);
            return;
        }
        case 2: {
            size = (Long) result
            toast("File is unzipped, size = " + size);
            return;
        }
    }
}

Как мы ранее уже обсудили, suspend-функция по завершении своей работы вызывает метод invokeSuspend(). И именно сюда же она и передает результат своей работы. Для этого у invokeSuspend() есть входной параметр типа Object.

Suspend-функция suspendedGetFile() при вызове invokeSuspend() передаст файл (File). А в ветке case 1 будет выполнено приведение типа Object к типу File.

Suspend-функция suspendUnzip() при вызове invokeSuspend() передаст размер (Long). А в ветке case 2 будет выполнено приведение типа Object к типу Long.

Suspend-функция

Давайте поговорим об одном из компонентов составляющих понятия Корутины Suspend-функции. Помните мы говорили о том что Корутины не блокируют поток, а приостанавливают работу кода. Ну так вот, такое свойство обеспечивает Suspend-функция. То есть, код который вписан в тело Suspend-функции при запуске не заблокирует поток, но этот код приостановит свою работу. Это означает что мы можем даже запустить Корутину в Main-потоке. Давайте приведем пример:

val url = "http://myurl.com/file.pdf"

// long synchronous function

getFile(url)

println("File is downloaded")

Простой пример, в котором пытаемся загрузить файл по определенному URL. Здесь функция getFile() синхронная, то есть блокирует поток. Но чтобы запускать функцию из основного потока нам нужно будет сделать ее асинхронной и прокинуть в нее лямбду-коллбек с функцией println(), который будет выполнен после загрузки файла:

getFile(url) { println("File is downloaded") }

Из выше сказанного следует что, при запуске массивного кода(с тяжелыми вычислениями) в основном потоке нам придется либо блокировать основной поток либо писать колбэк(и плодить колбэкхэлл)

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

CoroutineScope(Job()).launch {
	val url = "http://myurl.com/file.pdf"

	// long synchronous function
	suspendedGetFile(url)

	println("File is downloaded")
}

Тут launch — это Builders, который создает Корутину. В ее теле появилась Suspend-функция suspendedGetFile(url). Помните? Builder, Suspend-функция все это части Корутины. Эту конструкцию мы можем назвать Корутиной. Ну так вот, эта Корутина не блокирует поток и ее можно запустить в основном потоке. Suspend-функция suspendedGetFile() запустит загрузку файла в рабочем потоке, а функция println() отработает после завершения функции suspendedGetFile().

+ Suspend-функция возвращает свой ответ асинхронно поэтому ее нельзя вызвать из обычной функции
+ Suspend-функция может выполнять код в разных потоках. Нужно помнить про возможный рассинхрон, так как нет гарантии что исполнение и возврат ответа будет на одном потоке

Пусть это и выглядит как фантастика, но помните что Котлин-код далее будет трансформирован в Java-классы. И в процессе этих преобразований наша Корутина будет трансформирована и будет использован механизм Continuation. Все наши компоненты Корутины «под капотом» реализуют колбэк, который выполнит println() после загрузки suspendedGetFile().

Что же такое Корутина?

Для новичка-джуна, который еще плохо знает что такое потоки, синхронность-асинхронность, многопоточность, понять такую тему будет очень сложно. Особенно если в статьях-видосах начинают сразу же грузить такими терминами как Coroutine Builders, Scopes, Jobs и т.д.

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

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

Также как и капуста, свекла, картошка, лук, мясо — Builders, Suspend-функции, Scopes, Jobs, Dispatchers, Exception-Handler и др. классы работая вместе создают нам Корутину-борщ! =)

О чем и как работает Корутина? Если вам плохо знакомы такие понятия многопоточность, асинхронность, блокировка потока — Гуглите)

Корутина это Не про многопоточность, это про асинхронность. Корутина это Не про параллельность. Она не требуют блокирующих операций и блокировки потока. То есть, мы можем запустить Корутины на одном потоке при этом не заблокировав ее работу! Но приостановив работу кода.

Следовательно мы можем запустить Корутину и на UI-потоке в Андроиде, при этом его не заблокировав и не вызвав ANR! «Как такое возможно?» скажете вы, а благодаря такому понятию как Structered Concurrency или Структурированная Асинхронность, которое известно программистам еще 60-х годов прошлого века. Более подробно про механизм «остановки выполнения» поговорим чуть позже.

Корутины очень эффективно используют поток, в котором запускаются. Поэтому Корутины называют легковесными потоками. Но не стоит думать, что Корутины работают только в одном потоке. Их можно запускать в разных потоках. Для запуска Корутин в одном-многих потоках нам понядобятся Builders, Suspend-функции, Scopes, Jobs, Dispatchers. Но о них позже.

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

Корутины являются составной частью языка Котлин начиная с версии 1.1. Но в рамках языка реализованы лишь небольшая, базовая часть возможностей Корутин. Весь остальной функционал был вынесен в отдельную либу.

Итак, это вкратце и простым языком о Корутине=) Далее будем глубже изучать компоненты и запускать разные Корутины.

«Холодный» запуск приложения

Глубокий экскурс в процесс «холодного» запуска Android приложения, с момента нажатия на иконку и до создания процесса приложения.

Continue reading

© 2022 Nurlandroid

Theme by Anders NorénUp ↑