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 — callback, при чието извикване viewModelScope автоматично отменя всички активни корутини
  • clear() vs onCleared() — clear() се извиква от framework преди onCleared, което гарантира отмяна на scope
  • launch — основният начин за стартиране на корутини в viewModelScope за fire-and-forget операции

Какво е viewModelScope в Android?

viewModelScope — е extension свойство на интерфейса ViewModel, добавено в библиотеката lifecycle-viewmodel-ktx (от версия 2.1.0). Той предоставя готов CoroutineScope, свързан с жизнения цикъл на ViewModel.

kotlin
// Вътрешна структура (опростена)
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(). В този callback viewModelScope отменя своя Job, което рекурсивно прекратява всички активни корутини. Механизмът е имплементиран чрез интерфейса Closeable, където Job на scope се регистрира като ресурс за автоматично затваряне.

Как работи viewModelScope: свързване с жизнения цикъл на ViewModel

Механизмът за свързване на viewModelScope с жизнения цикъл на ViewModel се основава на маркиране и callback onCleared. Нека разгледаме стъпка по стъпка как работи.

Стъпка 1: Създаване на scope при първия достъп

Когато ViewModel изпълнява viewModelScope.launch { ... }, getter проверява дали има запазен scope под маркера JOB_KEY. Ако scope все още не е създаден — се създава нова инстанция CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Scope се съхранява вътре в ViewModel чрез вътрешна карта на маркери.

Стъпка 2: Жизнен цикъл на корутините

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

Стъпка 3: Отмяна при onCleared

Когато системата унищожава ViewModel, се извиква ViewModel.clear(). Вътре в clear() се случва следното:

  • Извикване на onCleared() за потребителска логика
  • Затваряне на всички Closeable ресурси, регистрирани чрез addCloseable
  • Job на viewModelScope преминава в състояние Cancelled
  • Всички дъщерни корутини се отменят рекурсивно
  • Освобождаване на референции към scope за garbage collector

Устойчивост на ротация на екрана

При завъртане на екрана 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 ms пауза във въвеждането. Това намалява натоварването на сървъра и предотвратява остарели резултати.

viewModelScope vs lifecycleScope: кога какво да изберем

И двата scope се предоставят от библиотеката AndroidX Lifecycle, но са свързани с различни жизнени цикли. Изборът между тях зависи от вида на задачата.

Сравнение на scope

ХарактеристикаviewModelScopelifecycleScope
СобственикViewModelLifecycleOwner (Activity/Fragment)
Отмяна при ротацияНе (ViewModel се запазва)Да (Activity се пресъздава)
Диспечер по подразбиранеDispatchers.Main.immediateDispatchers.Main.immediate
Достъпен вViewModelActivity, Fragment, Service
Типичен сценарийЗареждане на данни, бизнес логикаUI взаимодействие, анимации, snackbar

Препоръки на 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 се създава автоматично при следващия достъп до getter.

Обобщение

  • 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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също