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