Если вы только что закончили изучать корутины по документации или учебному курсу, то наверняка привыкли к простой схеме: функция main(), билдер runBlocking, пара вызовов delay() — и вот уже корутина делает что-то полезное. В Android этот уютный мир рушится в первую же минуту.
Функции main() здесь нет. Блокировать потоки нельзя — главный поток отвечает за отрисовку интерфейса, и если вы его остановите хотя бы на несколько секунд, система покажет пользователю диалог «Приложение не отвечает». А компоненты — Activity, Fragment, Service — рождаются и умирают по воле операционной системы, иногда в самый неподходящий момент.
Отсюда главное правило: корутина в Android всегда запускается в скоупе, привязанном к жизненному циклу того компонента, который её породил. Скоуп — это контейнер. Пока он жив, живут и его корутины. Умер — все корутины внутри автоматически отменяются.
Android предоставляет два готовых скоупа через библиотеки Jetpack: lifecycleScope и viewModelScope. Они решают разные задачи, и путаница между ними — одна из самых частых ошибок новичков.
lifecycleScope: спутник Activity и Fragment
У каждой Activity и каждого Fragment есть свой lifecycleScope. Он создаётся вместе с компонентом и уничтожается вместе с ним.
Допустим, мы хотим показывать на экране бегущую строку с прогнозом погоды, обновляя текст каждые три секунды:
class WeatherActivity : AppCompatActivity() {
private val forecasts = listOf(
"Москва: +12, облачно",
"Петербург: +8, дождь",
"Казань: +15, ясно"
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
val textView = findViewById<TextView>(R.id.textForecast)
lifecycleScope.launch {
var index = 0
while (isActive) {
textView.text = forecasts[index]
index = (index + 1) % forecasts.size
delay(3000)
}
}
}
}
Пока Activity на экране, корутина крутится и меняет текст. Стоит пользователю закрыть экран — скоуп уничтожается, корутина отменяется. Утечки памяти нет, лишняя работа не выполняется.
Но попробуйте повернуть телефон. Android уничтожит старую Activity и создаст новую. Вместе со старой Activity погибнет и её lifecycleScope, а значит, и корутина. Новая Activity запустит новую корутину — и бегущая строка начнётся сначала.
Для анимаций и обновления UI это нормальное поведение. Но если корутина выполняет сетевой запрос или пишет в базу данных, перезапуск при каждом повороте экрана — это катастрофа.
viewModelScope: переживает повороты
Здесь на сцену выходит viewModelScope. Он живёт внутри ViewModel — объекта, который Android сохраняет при пересоздании Activity. Поворот экрана уничтожает Activity, но не трогает ViewModel. Корутины, запущенные в viewModelScope, продолжают работать как ни в чём не бывало.
class WeatherViewModel : ViewModel() {
private val _forecast = MutableStateFlow("Загрузка...")
val forecast: StateFlow<String> = _forecast
init {
viewModelScope.launch {
val result = fetchForecastFromNetwork() // долгий сетевой запрос
_forecast.value = result
}
}
private suspend fun fetchForecastFromNetwork(): String {
delay(2000) // имитация сети
return "Москва: +12, облачно"
}
}
Даже если пользователь десять раз повернёт телефон, пока идёт запрос, — корутина не перезапустится. Когда результат будет готов, он попадёт в StateFlow, и новая Activity подхватит его.
Ещё одна важная деталь: viewModelScope под капотом создаётся с SupervisorJob. Это значит, что если одна из корутин внутри скоупа упадёт с исключением, остальные продолжат работать. В lifecycleScope поведение аналогичное, но для ViewModel это особенно критично: там часто запущено сразу несколько параллельных операций, и падение одной не должно утянуть за собой весь экран.
Что выбрать?
Правило простое. Если корутина обслуживает UI — анимации, таймеры, обновление текста, обработка жестов — ей место в lifecycleScope. Она привязана к конкретному экрану и должна умереть вместе с ним.
Если корутина работает с данными — сетевые запросы, чтение и запись в базу, тяжёлые вычисления — запускайте её в viewModelScope. Данные не должны зависеть от капризов системы.
| lifecycleScope | viewModelScope | |
|---|---|---|
| Привязан к | Activity / Fragment | ViewModel |
| Поворот экрана | Корутина отменяется | Корутина продолжает работу |
| Типичные задачи | Анимации, таймеры, обновление UI | Сеть, база данных, бизнес-логика |
| Диспетчер по умолчанию | Main | Main |
Обратите внимание: оба скоупа по умолчанию работают на диспетчере Main. Если внутри корутины вы обращаетесь к сети или базе данных, не забудьте переключить контекст:
viewModelScope.launch {
val data = withContext(Dispatchers.IO) {
repository.loadFromDatabase()
}
_state.value = data
}
А что в Compose?
В Jetpack Compose понятие lifecycleScope отходит на второй план. Ему на смену приходят LaunchedEffect и rememberCoroutineScope, которые привязывают корутины не к Activity, а к отдельным Composable-функциям. Но это тема для отдельного разговора.
А viewModelScope остаётся на месте — он по-прежнему лучший выбор для работы с данными, независимо от того, строите вы интерфейс на View или на Compose.