viewModelScope: що це, прив'язка до ViewModel та робота в Android

Автор: IT Sectr Опубліковано: 2026-06-23 Час читання: 9 хв

viewModelScope — це вбудований CoroutineScope з бібліотеки androidx.lifecycle, який прив'язаний до життєвого циклу ViewModel та автоматично скасовується при його очищенні. За даними Google Android Developers, 2025, viewModelScope є стандартним механізмом запуску корутин в MVVM-архітектурі, забезпечуючи безпечну роботу з асинхронними операціями без ризику витоку пам'яті. ViewModelScope використовує Dispatchers.Main за замовчуванням, а всі IO-операції всередині нього повинні виконуватися через withContext.

Головне

  • viewModelScope — CoroutineScope з lifecycle-viewmodel-ktx, що скасовується при onCleared() ViewModel
  • Dispatchers.Main — диспетчер за замовчуванням, тому UI-оновлення всередині корутин безпечні
  • onCleared — колбек, при виклику якого viewModelScope автоматично скасовує всі активні корутини
  • clear() vs onCleared() — clear() викликається фреймворком перед onCleared, що гарантує скасування scope
  • launch — основний спосіб запуску корутин в viewModelScope для fire-and-forget операцій

Що таке viewModelScope в Android?

viewModelScope — це extension-властивість на інтерфейс ViewModel, додана в бібліотеці lifecycle-viewmodel-ktx (починаючи з версії 2.1.0). Вона надає готовий CoroutineScope, прив'язаний до життєвого циклу ViewModel.

kotlin
// 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.

Як viewModelScope отримує повідомлення про очищення

Коли ViewModel залишає життєвий цикл (Activity завершена або Fragment видалено), система викликає clear(), який запускає onCleared(). У цьому колбеку viewModelScope скасовує свій Job, що рекурсивно завершує всі активні корутини. Механізм реалізовано через Closeable-інтерфейс, де Job scope реєструється як ресурс для автоматичного закриття.

Як працює viewModelScope: прив'язка до життєвого циклу ViewModel

Механізм прив'язки viewModelScope до життєвого циклу ViewModel заснований на тегуванні та колбеку onCleared. Розглянемо покроково, як це працює.

Крок 1: Створення scope при першому зверненні

Коли ViewModel виконує viewModelScope.launch { ... }, геттер перевіряє, чи є збережений scope за тегом JOB_KEY. Якщо scope ще не створено — створюється новий екземпляр CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Scope зберігається всередині ViewModel через внутрішню map тегів.

Крок 2: Життєвий цикл корутин

Всі корутини, запущені через viewModelScope.launch або viewModelScope.async, стають дочірніми по відношенню до SupervisorJob scope. Вони працюють на головному потоці (якщо не вказано інший диспетчер через withContext). Поки ViewModel жива — корутини можуть бути активні, призупинені або завершені.

Крок 3: Скасування при onCleared

Коли система знищує ViewModel, викликається ViewModel.clear(). Всередині clear() відбувається наступне:

  • Виклик onCleared() для користувацької логіки
  • Закриття всіх Closeable-ресурсів, зареєстрованих через addCloseable
  • Job viewModelScope переводиться в стан Cancelled
  • Всі дочірні корутини рекурсивно скасовуються
  • Звільнення посилань на scope для збирача сміття

Стійкість до ротації екрану

При повороті екрану Activity перестворюється, але ViewModel зберігається (завдяки ViewModelStoreOwner). Це означає, що viewModelScope залишається активним, і корутини продовжують виконуватися без переривання. Після перестворення Activity та ж ViewModel (і той же scope) використовується повторно — завантаження даних не починається заново.

viewModelScope в MVVM-архітектурі

MVVM (Model-View-ViewModel) — рекомендована Google архітектура для Android-додатків. viewModelScope займає в ній центральне місце як виконавець асинхронних операцій.

Роль viewModelScope в шарах архітектури

ШарКомпонентРоль viewModelScope
UIActivity / FragmentСпостерігає StateFlow/LiveData з ViewModel
ViewModelViewModelЗапускає корутини через viewModelScope, керує станом UI
RepositoryRepositoryПриймає suspend-функції, що викликаються з корутин viewModelScope
DataDAO / ApiВиконує фактичні запити (Room, Retrofit)

ViewModel через viewModelScope запускає корутини, всередині яких викликає suspend-функції Repository. Результат перетворюється в StateFlow, який спостерігається UI-шаром. Така схема забезпечує чіткий розподіл відповідальності та тестованість кожного шару незалежно.

Чому viewModelScope в ViewModel, а не в Fragment

Якби корутини запускалися з Fragment, при повороті екрану вони б скасовувалися разом із знищенням Fragment. ViewModel переживає ротацію, тому корутини, запущені в її scope, продовжують виконання. Це ключова перевага viewModelScope перед lifecycleScope при завантаженні даних.

Приклади використання viewModelScope

Розглянемо три практичні сценарії використання viewModelScope в Android-додатку на Kotlin.

Приклад 1: Завантаження даних при створенні ViewModel

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 не піклується про перемикання потоків.

Приклад 2: Обробка помилок через sealed class

kotlin
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}
kotlin
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 і реагує тільки на актуальний стан, ігноруючи застарілі виклики при повторних ротаціях.

Приклад 3: Скасування попередньої корутини при новому запиті

kotlin
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 мс паузи у вводі. Це знижує навантаження на сервер та запобігає застарілим результатам.

viewModelScope vs lifecycleScope: коли що вибирати

Обидва scope надаються бібліотекою AndroidX Lifecycle, але прив'язані до різних життєвих циклів. Вибір між ними залежить від типу задачі.

Порівняння scope

ХарактеристикаviewModelScopelifecycleScope
ВласникViewModelLifecycleOwner (Activity/Fragment)
Скасування при ротаціїНі (ViewModel зберігається)Так (Activity перестворюється)
Диспетчер за замовчуваннямDispatchers.Main.immediateDispatchers.Main.immediate
Доступний вViewModelActivity, Fragment, Service
Типовий сценарійЗавантаження даних, бізнес-логікаUI-взаємодія, анімації

Рекомендації Google

Google рекомендує використовувати viewModelScope для всіх завдань, пов'язаних із завантаженням та обробкою даних. lifecycleScope слід застосовувати для операцій, прив'язаних до конкретного моменту життя UI — наприклад, запуск анімації при першій появі екрану або підписка на Location-оновлення, яка повинна припинитися при виході з екрану.

Типові помилки при роботі з viewModelScope

Навіть у добре документованому API Android розробники допускають характерні помилки. Розглянемо чотири найбільш часті проблеми.

Помилка 1: Оновлення UI після скасування scope

Найпідступніша помилка — спроба оновити StateFlow або LiveData після того, як ViewModel очищена. Хоча viewModelScope скасовується при onCleared(), корутина може виконати код до моменту фактичного скасування. Використовуйте isActive для перевірки або покладайтеся на завершення catch-блоку.

Помилка 2: Запуск корутин без урахування SupervisorJob

viewModelScope використовує SupervisorJob всередині, що ізолює помилки між корутинами. Але якщо запустити корутину з власним Job() всередині viewModelScope.launch, ця корутина стане дочірньою до SupervisorJob, але не буде захищена від скасування при помилках в інших корутинах.

Помилка 3: Занадто багато корутин в одному scope

Хоча viewModelScope не має жорсткого ліміту, тисячі активних корутин можуть уповільнити систему. Для довгих списків даних використовуйте Flow з collectLatest замість створення окремих корутин під кожен елемент.

Помилка 4: Використання GlobalScope замість viewModelScope

Якщо випадково імпортувати GlobalScope замість viewModelScope, корутина не буде скасована при очищенні ViewModel. Це призведе до витоку пам'яті та потенційного крашу. Завжди перевіряйте, що корутини запускаються через viewModelScope, особливо в фрагментах-спадкоємцях.

Часті запитання

Чи можна змінити диспетчер viewModelScope за замовчуванням?

Безпосередньо змінити диспетчер у viewModelScope не можна — він жорстко заданий як Dispatchers.Main.immediate. Але всередині корутини можна переключитися на інший диспетчер через withContext. Для зміни диспетчера в тестах використовуйте TestDispatcher через Rule.

Як передати viewModelScope в Repository?

Не передавайте scope в Repository — це порушує архітектурні принципи. Repository повинен надавати suspend-функції, а ViewModel сама керує корутинами через viewModelScope. Якщо Repository вимагає scope — перегляньте архітектуру на користь Clean Architecture.

Чому viewModelScope використовує SupervisorJob?

SupervisorJob гарантує, що виняток в одній корутині (наприклад, помилка завантаження одного з декількох незалежних запитів) не скасовує інші корутини. Це відповідає сценарію ViewModel, де різні екрани завантажують незалежні дані.

Чи доступний viewModelScope в Jetpack Compose?

Так, viewModelScope доступний в будь-якій ViewModel незалежно від типу UI (View System або Jetpack Compose). В Compose корутини також запускаються через viewModelScope, а для UI-ефектів використовується LaunchedEffect та rememberCoroutineScope.

Що станеться з корутиною при виклику viewModelScope.cancel()?

Виклик viewModelScope.cancel() скасовує scope негайно — всі активні корутини завершуються з CancellationException. Якщо після цього викликати viewModelScope.launch, новий scope створюється автоматично при наступному зверненні до геттера.

Підсумки

  • viewModelScope — CoroutineScope, прив'язаний до життєвого циклу ViewModel та автоматично скасовуваний при onCleared()
  • SupervisorJob + Dispatchers.Main — внутрішня конфігурація, що забезпечує ізоляцію помилок та безпечний доступ до UI
  • Ротація екрану — ViewModel зберігається, тому корутини в viewModelScope продовжують роботу без перезапуску
  • MVVM-архітектура — viewModelScope є центральним елементом для асинхронних операцій в шарі ViewModel
  • lifecycleScope — альтернатива для операцій, прив'язаних до життєвого циклу Activity/Fragment, а не ViewModel
  • StateFlow — найкращий спосіб передачі даних з корутин viewModelScope в UI через sealed class
  • GlobalScope небезпечний — заміна viewModelScope на GlobalScope призводить до витоків пам'яті та падінь додатку

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також