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 property) на интерфејсу 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(). У овом колбеку viewModelScope отказује свој Job, што рекурзивно завршава све активне корутине. Механизам је имплементиран кроз Closeable интерфејс, где се Job scope-а региструје као ресурс за аутоматско затварање.

Како ради viewModelScope: повезивање са животним циклусом ViewModel

Механизам повезивања viewModelScope са животним циклусом ViewModel заснива се на ознакама и колбеку 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 за сакупљача отпада

Отпорност на ротацију екрана

При ротацији екрана, 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. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође