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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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