Побочные эффекты: LaunchedEffect

Узнаём, как правильно запускать побочные эффекты (загрузку данных, корутины) в Compose, не нарушая правила recomposition.

Побочный эффект — действие, выходящее за пределы отрисовки UI: сетевой запрос, запись в базу, запуск таймера или корутины.

Почему эффекты нельзя в теле composable

Как мы помним, тело composable может выполняться много раз. Если положить туда сетевой запрос, он повторится на каждой перерисовке. Для управляемого запуска эффектов Compose даёт специальные функции — effect handlers.

LaunchedEffect — запуск при входе

LaunchedEffect запускает корутину, когда composable впервые появляется на экране, и отменяет её при уходе. Он принимает ключ: при его смене эффект перезапускается:

@Composable
fun UserScreen(userId: String, viewModel: UserViewModel) {
    LaunchedEffect(userId) {
        viewModel.loadUser(userId)   // выполнится при входе и при смене userId
    }
    // ... отрисовка экрана
}

Здесь загрузка пользователя случится один раз при появлении экрана и повторно — только если изменится userId. На обычных перерисовках (например, при наборе текста в другом поле) эффект не сработает.

rememberCoroutineScope — эффект по событию

LaunchedEffect хорош для запуска «при входе». А если корутину нужно запустить в ответ на клик? Тогда берут rememberCoroutineScope — он даёт scope, привязанный к жизни composable:

@Composable
fun SnackButton(snackbarHostState: SnackbarHostState) {
    val scope = rememberCoroutineScope()
    Button(onClick = {
        scope.launch {
            snackbarHostState.showSnackbar("Готово!")
        }
    }) {
        Text("Показать сообщение")
    }
}

Когда что использовать

ИнструментКогда
LaunchedEffect(key)запустить при входе / смене ключа
rememberCoroutineScopeзапустить корутину по событию (клик)

Как работает под капотом

LaunchedEffect привязывает корутину к месту в композиции. Пока composable на экране и ключ не менялся, корутина продолжает жить. Уход composable из дерева отменяет её автоматически — это спасает от утечек и «висящих» запросов. rememberCoroutineScope возвращает scope, который тоже отменяется при выходе composable, но запускать корутины в нём вы можете вручную, из любого обработчика.

Частые ошибки

  • Класть загрузку данных прямо в тело composable — она повторится при каждой перерисовке.
  • Запускать в onClick корутину через произвольный scope вместо rememberCoroutineScope — рискуете утечкой после ухода экрана.
  • Передавать в LaunchedEffect неизменный ключ Unit, когда эффект должен реагировать на смену параметра — он не перезапустится.

Итог

  • Побочные эффекты нельзя писать в теле composable — только через effect handlers.
  • LaunchedEffect(key) запускает корутину при входе и перезапускает при смене ключа.
  • rememberCoroutineScope даёт scope для запуска корутин по событиям (клик).
Проверьте себя
1. Когда срабатывает LaunchedEffect?
AНа каждой перерисовке
BПри входе composable на экран и при смене переданного ключа
CТолько при нажатии кнопки
DОдин раз за всё время работы приложения
2. Что использовать для запуска корутины в ответ на клик?
ALaunchedEffect(Unit)
BrememberCoroutineScope и его scope.launch в onClick
CОбычную функцию без корутин
Dremember { mutableStateOf }