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.
// Вътрешна структура (опростена)
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(). В този callback viewModelScope отменя своя Job, което рекурсивно прекратява всички активни корутини. Механизмът е имплементиран чрез интерфейса Closeable, където Job на scope се регистрира като ресурс за автоматично затваряне.
Механизмът за свързване на viewModelScope с жизнения цикъл на ViewModel се основава на маркиране и callback onCleared. Нека разгледаме стъпка по стъпка как работи.
Когато ViewModel изпълнява viewModelScope.launch { ... }, getter проверява дали има запазен scope под маркера JOB_KEY. Ако scope все още не е създаден — се създава нова инстанция CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Scope се съхранява вътре в ViewModel чрез вътрешна карта на маркери.
Всички корутини, стартирани чрез 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 ms пауза във въвеждането. Това намалява натоварването на сървъра и предотвратява остарели резултати.
И двата scope се предоставят от библиотеката AndroidX Lifecycle, но са свързани с различни жизнени цикли. Изборът между тях зависи от вида на задачата.
| Характеристика | viewModelScope | lifecycleScope |
|---|---|---|
| Собственик | ViewModel | LifecycleOwner (Activity/Fragment) |
| Отмяна при ротация | Не (ViewModel се запазва) | Да (Activity се пресъздава) |
| Диспечер по подразбиране | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Достъпен в | ViewModel | Activity, Fragment, Service |
| Типичен сценарий | Зареждане на данни, бизнес логика | UI взаимодействие, анимации, snackbar |
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 се създава автоматично при следващия достъп до getter.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също