В мире View корутины запускались из чётко определённых точек: onCreate(), onResume(), onDestroy(). В Compose этих методов нет. Composable-функции вызываются многократно, перестраиваются при каждом изменении состояния и не гарантируют стабильного жизненного цикла. Запустить корутину прямо в теле Composable-функции — всё равно что поставить чайник на плиту, которая включается и выключается сама по себе.
Compose предлагает два инструмента для запуска корутин: LaunchedEffect и rememberCoroutineScope. Они решают разные задачи, и выбор между ними зависит от того, кто инициирует запуск корутины — сам Compose или пользователь.
LaunchedEffect: корутина, привязанная к жизни Composable
LaunchedEffect — это Composable-функция, которая запускает корутину при первом появлении на экране и автоматически отменяет её, когда Composable исчезает из композиции.
Простейший пример — секундомер:
@Composable
fun Stopwatch() {
var seconds by remember { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
seconds++
}
}
Text(
text = "Прошло: $seconds с",
fontSize = 24.sp
)
}
Корутина стартует один раз, когда Stopwatch() попадает на экран. Каждую секунду она увеличивает счётчик, Compose перерисовывает текст. Когда пользователь уходит с экрана и Stopwatch() покидает композицию — корутина отменяется.
Аргумент Unit, который мы передаём в LaunchedEffect, — это ключ. Он определяет, при каких условиях корутина будет перезапущена. Если ключ не меняется, корутина живёт спокойно. Если ключ изменился — старая корутина отменяется, и на её месте стартует новая.
Допустим, мы хотим загружать описание товара каждый раз, когда пользователь выбирает другой элемент из списка:
@Composable
fun ProductDetails(productId: Int) {
var description by remember { mutableStateOf("Загрузка...") }
LaunchedEffect(productId) {
description = "Загрузка..."
delay(1000) // имитация сетевого запроса
description = "Описание товара #$productId"
}
Text(description)
}
Ключ здесь — productId. Пока пользователь смотрит один и тот же товар, корутина не перезапускается. Как только productId изменился, старая корутина отменяется (даже если запрос ещё не завершился), и стартует новая — уже с актуальным идентификатором.
Ключей может быть несколько:
LaunchedEffect(userId, language) {
// перезапустится, если изменится userId ИЛИ language
}
Раньше у LaunchedEffect была версия без ключей — просто LaunchedEffect { ... }. Сейчас она помечена как deprecated. Даже если перезапуск вам не нужен, передайте хотя бы фиктивный ключ Unit.
rememberCoroutineScope: корутина по запросу
LaunchedEffect хорош, когда корутина должна стартовать автоматически, вместе с появлением Composable на экране. Но что делать, если корутину нужно запустить по нажатию кнопки?
Вызвать launch прямо из onClick нельзя — у нас нет скоупа. Вот тут и пригодится rememberCoroutineScope:
@Composable
fun SaveButton(viewModel: SettingsViewModel) {
val scope = rememberCoroutineScope()
var isSaving by remember { mutableStateOf(false) }
Button(
onClick = {
scope.launch {
isSaving = true
viewModel.saveSettings()
isSaving = false
}
},
enabled = !isSaving
) {
Text(if (isSaving) "Сохранение..." else "Сохранить")
}
}
Функция rememberCoroutineScope() создаёт скоуп, привязанный к текущему Composable. Пока SaveButton на экране, скоуп жив. Исчезнет SaveButton — скоуп отменит все свои корутины.
Важное отличие от LaunchedEffect: корутина стартует не автоматически, а только когда мы сами вызовем scope.launch { }. Это даёт нам полный контроль над моментом запуска.
Когда что использовать
Разница между этими инструментами — в инициаторе запуска:
| LaunchedEffect | rememberCoroutineScope | |
|---|---|---|
| Запуск | Автоматический, при входе в композицию | Ручной, по событию (клик, жест) |
| Перезапуск | При изменении ключей | Только при повторном вызове launch |
| Отмена | При выходе из композиции или смене ключей | При выходе из композиции |
| Типичные задачи | Загрузка данных, подписка на события, таймеры | Отправка формы, сохранение, навигация по клику |
Есть простой способ не ошибиться: если корутина должна работать всегда, пока виден экран, — используйте LaunchedEffect. Если она нужна в ответ на действие пользователя — берите rememberCoroutineScope.
Частая ошибка: launch внутри Composable
Иногда возникает соблазн написать так:
// не делайте так
@Composable
fun BadExample() {
val scope = rememberCoroutineScope()
scope.launch {
// корутина запускается при каждой рекомпозиции!
}
}
При каждой рекомпозиции будет создаваться новая корутина, а старая продолжит работать. Через минуту у вас будут десятки одинаковых корутин, дружно пожирающих ресурсы.
Если корутина должна стартовать автоматически — используйте LaunchedEffect, а не голый launch в теле Composable. А rememberCoroutineScope вызывайте только из обработчиков событий: onClick, onValueChange и им подобных.
Оба этих инструмента работают на стороне UI. Для долгих операций с данными — сетевых запросов, работы с базой — по-прежнему лучше использовать viewModelScope внутри ViewModel. А LaunchedEffect и rememberCoroutineScope подключат результат этой работы к интерфейсу.