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-а должен хранить в себе Job-у scope-а. 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 в примере выше).