viewModelScope — то је уграђени CoroutineScope из библиотеке androidx.lifecycle, који је повезан са животним циклусом ViewModel и аутоматски се отказује при његовом чишћењу. Према Google Android Developers, 2025, viewModelScope је стандардни механизам за покретање корутина у MVVM архитектури, обезбеђујући безбедан рад са асинхроним операцијама без ризика од цурења меморије. ViewModelScope подразумевано користи Dispatchers.Main, а све IO операције унутар њега морају се извршавати кроз withContext.
Главне тачке
viewModelScope — то је проширујуће својство (extension property) на интерфејсу 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(). У овом колбеку viewModelScope отказује свој Job, што рекурзивно завршава све активне корутине. Механизам је имплементиран кроз Closeable интерфејс, где се Job scope-а региструје као ресурс за аутоматско затварање.
Механизам повезивања viewModelScope са животним циклусом ViewModel заснива се на ознакама и колбеку 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. године. Саветоваћемо вас и предложити најбоље решење.
Прочитајте такође