withContext — це функція-перемикач контексту виконання всередині співпрограми, яка тимчасово змінює потік або диспетчер для вказаного блоку коду та повертає результат назад у вихідний контекст. За даними JetBrains, 2025, withContext — один із найбільш часто використовуваних інструментів корутин для роботи з мережевими запитами та дисковими операціями. Функція гарантує, що після завершення блоку корутина продовжить виконання на вихідному диспетчері, що запобігає випадковим помилкам потокобезпеки.
Головне
withContext — це призупиняюча функція з пакету kotlinx.coroutines, яка виконує переданий блок коду в заданому CoroutineContext та повертає результат назад у вихідний контекст. Сигнатура функції виглядає так:
suspend fun withContext (
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
Параметр context приймає будь-який CoroutineContext — найчастіше один зі стандартних Dispatchers.IO, Dispatchers.Default або Dispatchers.Main. Блок виконується саме в цьому контексті, а результат повертається туди, звідки був викликаний withContext.
Після завершення лямбди withContext гарантовано перемикає виконання назад на вихідний диспетчер. Це означає, що розробнику не потрібно вручну викликати withContext(Dispatchers.Main) після фонової операції — повернення відбувається автоматично. Дана поведінка зафіксована у специфікації Kotlin Coroutines починаючи з версії 1.3.
Android-розробка — основна область застосування withContext. Типовий сценарій: ViewModel запускає корутину на головному потоці, всередині викликається withContext(Dispatchers.IO) для мережевого запиту, а результат після автоматичного повернення на Main використовується для оновлення UI. Такий підхід лежить в основі архітектури MVVM та рекомендується Google в офіційному гайді по корутинах.
Щоб зрозуміти withContext, потрібно розібратися з CoroutineContext та його ключовим компонентом — диспетчером (Dispatcher). Кожна співпрограма має набір елементів контексту, серед яких диспетчер визначає, на якому потоці чи пулі потоків виконується код.
| Диспетчер | Призначення | Розмір пулу |
|---|---|---|
| Dispatchers.Main | Головний потік UI (Android, JavaFX, Swing) | 1 (головний потік) |
| Dispatchers.IO | Дискові та мережеві операції | 64 потоки (ліміт зростає) |
| Dispatchers.Default | CPU-інтенсивні обчислення | max(2, кількість ядер) |
| Dispatchers.Unconfined | Без фіксованого потоку | не обмежений |
Важливо розуміти, що withContext не створює нову корутину — він тільки перемикає контекст для існуючої. Це ключова відмінність від launch та async, які породжують нові співпрограми. Внутрішня реалізація withContext оптимізована: якщо запитаний контекст збігається з поточним, перемикання не відбувається — функція виконується на тому ж диспетчері.
Dispatchers.Main всередині withContext(Dispatchers.Main) не викликає перемикання — Kotlin Coroutines розпізнає ідентичність контекстів та пропускає зайву операцію. Аналогічно withContext(Dispatchers.Default) всередині корутини, яка вже працює на Default, не створює накладних витрат. Ця оптимізація реалізована в ContinuationInterceptor.
Новачки часто плутають withContext з launch та async, оскільки всі три функції працюють з корутинами та контекстом. Однак їх призначення принципово різниться.
| Характеристика | withContext | launch | async |
|---|---|---|---|
| Створює нову корутину | Ні | Так | Так |
| Повертає результат | Так (T безпосередньо) | Ні (Job) | Так (Deferred<T>) |
| Виконання | Послідовне | Паралельне | Паралельне |
| Очікування результату | Автоматичне | join() | await() |
| Типовий use-case | Зміна диспетчера | Fire-and-forget | Паралельні обчислення |
Якщо потрібно виконати одну операцію у фоновому потоці та отримати результат — використовуйте withContext. Якщо потрібно запустити кілька незалежних операцій паралельно — використовуйте async з await. Якщо результат не потрібен (логіювання, запис кешу) — launch. Google рекомендує withContext як кращий інструмент для Repository-шару в архітектурі Android.
Розглянемо три практичні сценарії використання withContext в Android-додатках на Kotlin. Кожен приклад демонструє конкретну задачу та правильний патерн.
ViewModel викликає метод репозиторію з корутини на Main. Всередині withContext(Dispatchers.IO) виконується HTTP-запит, а результат повертається автоматично:
class UserRepository(
private val api: UserApi
) {
suspend fun getUser(id: String): User {
return withContext(Dispatchers.IO) {
api.fetchUser(id)
}
}
}
Корутина в ViewModel викликає getUser так само, як звичайну призупиняючу функцію — без явного вказання диспетчера. withContext приховує деталі перемикання потоків.
Коли потрібно виконати кілька IO-операцій одну за одною, withContext об'єднує їх в єдиний блок. Це ефективніше, ніж обгортати кожну операцію в окремий withContext:
suspend fun loadUserProfile(id: String): Profile {
return withContext(Dispatchers.IO) {
val user = api.fetchUser(id)
val posts = api.fetchPosts(id)
Profile(user, posts)
}
}
Обидві операції виконуються на Dispatchers.IO, а результат Profile створюється та повертається без зайвих перемикань контексту. Якщо операції незалежні, краще використовувати async для паралельного виконання.
У деяких сценаріях потрібно виконати код, який не можна скасувати — наприклад, збереження стану при закритті екрану. Комбінація withContext + NonCancellable вирішує це завдання:
withContext(Dispatchers.IO + NonCancellable) {
cache.saveState(state)
analytics.logEvent("state_saved")
}
Оператор + об'єднує два елементи контексту: IO-диспетчер та прапор NonCancellable. Блок виконується навіть якщо батьківська корутина була скасована — це корисно для фіналізуючих операцій.
Внутрішня реалізація withContext спирається на механізм Continuation — центральну абстракцію корутин Kotlin. Кожна точка призупинення (suspend point) зберігає стан виконання в об'єкт Continuation, і withContext не виняток.
Компілятор Kotlin транслює withContext у виклик методу withContext з kotlinx.coroutines, який всередині створює новий екземпляр DispatchedContinuation. Цей об'єкт обгортає вихідний Continuation та замінює в ньому dispatcher. Якщо новий dispatcher відрізняється від поточного, виконання призупиняється, блок надсилається у відповідний пул потоків, а після завершення — відновлюється з вихідним контекстом.
Коли withContext викликається з тим самим диспетчером, на якому вже виконується корутина, Kotlin запускає fast-path: блок виконується синхронно, без створення DispatchedContinuation та без передачі в пул потоків. Це робить withContext практично безкоштовним при повторних викликах з однаковим контекстом. За даними бенчмарків JetBrains (kotlinx.coroutines 1.8), fast-path виконується менш ніж за 0.1 мкс.
Кожен виклик withContext з іншим диспетчером створює новий DispatchedContinuation та потребує перемикання потоків — це займає від 1 до 5 мкс залежно від навантаження. Для більшості додатків така затримка непомітна, але всередині циклів з тисячами ітерацій варто агрегувати операції в один блок withContext.
Навіть досвідчені розробники допускають помилки при роботі з withContext. Розглянемо чотири найбільш поширені проблеми та способи їх запобігання.
Розробники часто обгортають кожен рядок в окремий withContext, замість того щоб об'єднати операції в один блок. Кожен зайвий виклик з іншим диспетчером створює накладні витрати.
Правильно: об'єднати послідовні IO-операції в один withContext(Dispatchers.IO) { ... }. Якщо частина операцій CPU-інтенсивна — використовуйте withContext(Dispatchers.Default) всередині того ж блоку.
withContext виконує код послідовно. Якщо два незалежних мережевих запити обгорнуті в один withContext, вони будуть виконуватися один за одним. Для паралельності використовуйте async + await.
// Послідовно — повільно
withContext(Dispatchers.IO) {
val a = api.fetchA()
val b = api.fetchB()
}
// Паралельно — швидко
coroutineScope {
val a = async { api.fetchA() }
val b = async { api.fetchB() }
println("${a.await()} ${b.await()}")
}
Якщо корутина скасовується під час withContext, блок на Dispatchers.IO теж переривається. Для операцій, які повинні завершитися будь-якою ціною (запис в БД, відправка аналітики), комбінуйте withContext з NonCancellable.
Ніколи не оновлюйте View-компоненти всередині withContext(Dispatchers.IO). withContext не повертає на Main до завершення всього блоку. Виносьте оновлення UI після закриваючої дужки withContext — тоді корутина вже буде на головному потоці.
Часто задавані питання
withContext — призупиняюча функція, яка не блокує потік, а перемикає контекст всередині існуючої корутини. runBlocking — міст між корутинами та звичайним кодом, який блокує поточний потік до завершення. withContext безпечний для UI-потоку, runBlocking — ні.
Ні, withContext — suspend-функція, тому її можна викликати тільки з іншої suspend-функції або з корутини (launch/async). Зі звичайної функції withContext не викликається — для цього потрібен runBlocking або CoroutineScope.
Kotlin активує fast-path — блок виконується синхронно на тому ж потоці без перемикання. Накладні витрати менше 0.1 мкс. Це не є помилкою, але такий виклик надлишковий — краще просто виконати код без withContext.
Винятки всередині withContext проброшуються так само, як у звичайному коді — через try-catch. Якщо блок викинув виняток, він поширюється в батьківську корутину та скасовує її, якщо не оброблений. Використовуйте try-catch всередині withContext або навколо нього.
Ні, withContext не створює нової корутини. Він використовує існуючу співпрограму, але тимчасово змінює її контекст. Це відрізняє його від launch та async, які породжують дочірні корутини. Поведінка підтверджена вихідним кодом kotlinx.coroutines.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також