Статья, объясняющая, что такое CoroutineContext в Kotlin и что с этим делать.
РЕКОМЕНДУЕМ:
- Оптимизация загрузки приложений в Kotlin
- Современный подход к разработке с использованием Kotlin
- Как сделать код на Kotlin более понятным
Если взглянуть на определение любого билдера корутин (launch, async, runBlocking и т. д.) в коде, то можно увидеть, что их первый аргумент — это CoroutineContext:
1 2 3 4 5 6 7 |
public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { ... } |
Также мы видим, что launch — это функция‑расширение CoroutineScope. И ее последний аргумент (собственно, сам блок кода) тоже выполняется в области видимости CoroutineScope.
А теперь взглянем на определение самого CoroutineScope:
1 2 3 4 5 |
public interface CoroutineScope { public val coroutineContext: CoroutineContext } По сути, это просто враппер для [crayon-6706ce38cad5a270521626 inline="true" ]CoroutineContext |
. Даже
Continuation, олицетворяющий собой корутины, тоже содержит в себе
CoroutineContext:
public interface Continuation { public val context: CoroutineContext public fun resumeWith(result: Result)}[/crayon]
Другими словами, CoroutineContext — это самый важный элемент корутины. Но что это такое?
На самом деле CoroutineContext — это интерфейс для коллекции элементов Element, где Element — это Job, CoroutineName, CouroutineDispatcher, SupervisorJob, CoroutineExceptionHandler или сам CoroutineContext.
Элементы этой коллекции имеют уникальный ключ. А для извлечения элементов можно использовать getили квадратные скобки:
1 2 |
val ctx: CoroutineContext = CoroutineName("A name") val coroutineName: CoroutineName? = ctx[CoroutineName] |
Как видно, в качестве ключа можно использовать просто имя класса. В Kotlin имя класса — это ссылка на объект‑компаньон этого класса, а в случае с CoroutineContext объект‑компаньон как раз и содержит ключ:
1 2 3 4 5 6 |
data class CoroutineName( val name: String ) : AbstractCoroutineContextElement(CoroutineName) { override fun toString(): String = "CoroutineName($name)" companion object Key : CoroutineContext.Key } |
Также CoroutineContext можно складывать, при этом элементы с совпадающими ключами будут заменены на новые:
1 2 3 4 5 6 7 8 9 10 11 |
fun main() { val ctx1: CoroutineContext = CoroutineName("Name1") println(ctx1[CoroutineName]?.name) // Name1 println(ctx1[Job]?.isActive) // null val ctx2: CoroutineContext = Job() println(ctx2[CoroutineName]?.name) // null println(ctx2[Job]?.isActive) // true val ctx3 = ctx1 + ctx2 println(ctx3[CoroutineName]?.name) // Name1 println(ctx3[Job]?.isActive) // true } |
В целом CoroutineContext — это способ передачи данных между корутинами. Такая передача, например, осуществляется при запуске дочерней корутины:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
fun CoroutineScope.log(msg: String) { val name = coroutineContext[CoroutineName]?.name println("[$name] $msg") } fun main() = runBlocking(CoroutineName("main")) { log("Started") // [main] Started val v1 = async { delay(500) log("Running async") // [main] Running async 42 } launch { delay(1000) log("Running launch") // [main] Running launch } log("The answer is ${v1.await()}") // [main] The answer is 42 } |
Это код показывает, как заданное нами имя корутины (main) в итоге передается всем ее потомкам. Но если мы захотим, мы можем изменить это имя:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fun main() = runBlocking(CoroutineName("main")) { log("Started") // [main] Started val v1 = async(CoroutineName("c1")) { delay(500) log("Running async") // [c1] Running async 42 } launch(CoroutineName("c2")) { delay(1000) log("Running launch") // [c2] Running launch } log("The answer is ${v1.await()}") // [main] The answer is 42 } |
По сути, правило здесь простое: контекст корутины = контекст родителя + контекст потомка.
Зачем все это нужно знать? Например, для того, чтобы понимать, что делает всем известная функция withContext. Обычно ее используют для запуска кода в другом потоке, например:
1 2 3 |
withContext(Dispatchers.Default) { // здесь тяжелый код } |
Эта функция просто прибавляет к существующему CorotuneContext диспатчер Default, таким образом заменяя прошлый диспатчер. Далее она запускает новую корутину уже в этом контексте, и корутина начинает работу в другом потоке.
Теперь ты должен понимать, что вместо диспетчера в аргументе этой функции можно передать любой другой Element, например тот же CoroutineName. То есть с ее помощью можно не только перенаправлять корутины в новые потоки, но и менять любые их характеристики.
What is CoroutineContext and how does it work?