viewModelScope — це вбудований CoroutineScope з бібліотеки androidx.lifecycle, який прив'язаний до життєвого циклу ViewModel та автоматично скасовується при його очищенні. За даними Google Android Developers, 2025, viewModelScope є стандартним механізмом запуску корутин в MVVM-архітектурі, забезпечуючи безпечну роботу з асинхронними операціями без ризику витоку пам'яті. ViewModelScope використовує Dispatchers.Main за замовчуванням, а всі IO-операції всередині нього повинні виконуватися через withContext.
Головне
viewModelScope — це extension-властивість на інтерфейс ViewModel, додана в бібліотеці lifecycle-viewmodel-ktx (починаючи з версії 2.1.0). Вона надає готовий CoroutineScope, прив'язаний до життєвого циклу ViewModel.
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope = this.getTag(JOB_KEY)
if (scope != null) return scope
return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { setTag(JOB_KEY, it) }
}
Scope створюється ліниво (lazy) при першому зверненні та кешується через setTag. Використовується SupervisorJob, що означає, що виняток в одній дочірній корутині не скасовує інші. Диспетчер за замовчуванням — Dispatchers.Main.immediate, який виконує код на головному потоці без додаткової диспетчеризації, якщо виклик вже на Main.
Коли ViewModel залишає життєвий цикл (Activity завершена або Fragment видалено), система викликає clear(), який запускає onCleared(). У цьому колбеку viewModelScope скасовує свій Job, що рекурсивно завершує всі активні корутини. Механізм реалізовано через Closeable-інтерфейс, де Job scope реєструється як ресурс для автоматичного закриття.
Механізм прив'язки viewModelScope до життєвого циклу ViewModel заснований на тегуванні та колбеку onCleared. Розглянемо покроково, як це працює.
Коли ViewModel виконує viewModelScope.launch { ... }, геттер перевіряє, чи є збережений scope за тегом JOB_KEY. Якщо scope ще не створено — створюється новий екземпляр CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Scope зберігається всередині ViewModel через внутрішню map тегів.
Всі корутини, запущені через viewModelScope.launch або viewModelScope.async, стають дочірніми по відношенню до SupervisorJob scope. Вони працюють на головному потоці (якщо не вказано інший диспетчер через withContext). Поки ViewModel жива — корутини можуть бути активні, призупинені або завершені.
Коли система знищує ViewModel, викликається ViewModel.clear(). Всередині clear() відбувається наступне:
При повороті екрану Activity перестворюється, але ViewModel зберігається (завдяки ViewModelStoreOwner). Це означає, що viewModelScope залишається активним, і корутини продовжують виконуватися без переривання. Після перестворення Activity та ж ViewModel (і той же scope) використовується повторно — завантаження даних не починається заново.
MVVM (Model-View-ViewModel) — рекомендована Google архітектура для Android-додатків. viewModelScope займає в ній центральне місце як виконавець асинхронних операцій.
| Шар | Компонент | Роль viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Спостерігає StateFlow/LiveData з ViewModel |
| ViewModel | ViewModel | Запускає корутини через viewModelScope, керує станом UI |
| Repository | Repository | Приймає suspend-функції, що викликаються з корутин viewModelScope |
| Data | DAO / Api | Виконує фактичні запити (Room, Retrofit) |
ViewModel через viewModelScope запускає корутини, всередині яких викликає suspend-функції Repository. Результат перетворюється в StateFlow, який спостерігається UI-шаром. Така схема забезпечує чіткий розподіл відповідальності та тестованість кожного шару незалежно.
Якби корутини запускалися з Fragment, при повороті екрану вони б скасовувалися разом із знищенням Fragment. ViewModel переживає ротацію, тому корутини, запущені в її scope, продовжують виконання. Це ключова перевага viewModelScope перед lifecycleScope при завантаженні даних.
Розглянемо три практичні сценарії використання viewModelScope в Android-додатку на Kotlin.
class ProfileViewModel(
private val repo: ProfileRepository
) : ViewModel() {
private val _profile = MutableStateFlow<Profile?>(null)
val profile: StateFlow<Profile?> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val result = repo.getProfile()
_profile.value = result
}
}
}
В init блоці одразу запускається завантаження профілю. Корутина виконується на головному потоці (за замовчуванням). Репозиторій використовує withContext(Dispatchers.IO) для мережевого запиту всередині своєї suspend-функції, тому ViewModel не піклується про перемикання потоків.
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
fun fetchItems() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val items = repo.getItems()
_state.value = UiState.Success(items)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
Стан UI описується через sealed class UiState. ViewModel оновлює state при кожній зміні. Fragment підписаний на StateFlow і реагує тільки на актуальний стан, ігноруючи застарілі виклики при повторних ротаціях.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
При кожному новому пошуковому запиті попередня корутина скасовується. delay(300) реалізує debounce — пошук виконується тільки після 300 мс паузи у вводі. Це знижує навантаження на сервер та запобігає застарілим результатам.
Обидва scope надаються бібліотекою AndroidX Lifecycle, але прив'язані до різних життєвих циклів. Вибір між ними залежить від типу задачі.
| Характеристика | viewModelScope | lifecycleScope |
|---|---|---|
| Власник | ViewModel | LifecycleOwner (Activity/Fragment) |
| Скасування при ротації | Ні (ViewModel зберігається) | Так (Activity перестворюється) |
| Диспетчер за замовчуванням | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Доступний в | ViewModel | Activity, Fragment, Service |
| Типовий сценарій | Завантаження даних, бізнес-логіка | UI-взаємодія, анімації |
Google рекомендує використовувати viewModelScope для всіх завдань, пов'язаних із завантаженням та обробкою даних. lifecycleScope слід застосовувати для операцій, прив'язаних до конкретного моменту життя UI — наприклад, запуск анімації при першій появі екрану або підписка на Location-оновлення, яка повинна припинитися при виході з екрану.
Навіть у добре документованому API Android розробники допускають характерні помилки. Розглянемо чотири найбільш часті проблеми.
Найпідступніша помилка — спроба оновити StateFlow або LiveData після того, як ViewModel очищена. Хоча viewModelScope скасовується при onCleared(), корутина може виконати код до моменту фактичного скасування. Використовуйте isActive для перевірки або покладайтеся на завершення catch-блоку.
viewModelScope використовує SupervisorJob всередині, що ізолює помилки між корутинами. Але якщо запустити корутину з власним Job() всередині viewModelScope.launch, ця корутина стане дочірньою до SupervisorJob, але не буде захищена від скасування при помилках в інших корутинах.
Хоча viewModelScope не має жорсткого ліміту, тисячі активних корутин можуть уповільнити систему. Для довгих списків даних використовуйте Flow з collectLatest замість створення окремих корутин під кожен елемент.
Якщо випадково імпортувати GlobalScope замість viewModelScope, корутина не буде скасована при очищенні ViewModel. Це призведе до витоку пам'яті та потенційного крашу. Завжди перевіряйте, що корутини запускаються через viewModelScope, особливо в фрагментах-спадкоємцях.
Часті запитання
Безпосередньо змінити диспетчер у viewModelScope не можна — він жорстко заданий як Dispatchers.Main.immediate. Але всередині корутини можна переключитися на інший диспетчер через withContext. Для зміни диспетчера в тестах використовуйте TestDispatcher через Rule.
Не передавайте scope в Repository — це порушує архітектурні принципи. Repository повинен надавати suspend-функції, а ViewModel сама керує корутинами через viewModelScope. Якщо Repository вимагає scope — перегляньте архітектуру на користь Clean Architecture.
SupervisorJob гарантує, що виняток в одній корутині (наприклад, помилка завантаження одного з декількох незалежних запитів) не скасовує інші корутини. Це відповідає сценарію ViewModel, де різні екрани завантажують незалежні дані.
Так, viewModelScope доступний в будь-якій ViewModel незалежно від типу UI (View System або Jetpack Compose). В Compose корутини також запускаються через viewModelScope, а для UI-ефектів використовується LaunchedEffect та rememberCoroutineScope.
Виклик viewModelScope.cancel() скасовує scope негайно — всі активні корутини завершуються з CancellationException. Якщо після цього викликати viewModelScope.launch, новий scope створюється автоматично при наступному зверненні до геттера.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також