Идеология Compose такова что @Composable-функции должны быть «чистыми», то есть «меняется состояние => меняется UI». И ничего постороннего.
Но в реальной жизни нужно логировать, дергать API, запускать корутины, подписываться на коллбеки и т.д. Для этого Compose даёт специальные эффекты, которые жёстко привязаны к жизненному циклу композиции, не ломают рекомпозицию и дают понятные точки: входа/обновления/выхода из композиции.
LaunchedEffect — запуск корутины, привязанной к композиции
Что делает:
- Стартует корутину, когда composable входит в композицию
- Автоматически отменяет её, когда ключ меняется или composable уходит из композиции
@Composable
fun UserScreen(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
// эта корутина:
// - запустится при первой композиции
// - перезапустится при изменении userId
// - отменится при уходе UserScreen из composition
user = repository.loadUser(userId)
}
...
}
При изменении ключа старая корутина отменяется и запускается новая. Плюс, корректно «подчиняется» lifecycle-у композиции, то есть нет утечек типа «корутина еще работает, а UI уже нет».
Использовать, когда нужно асинхронное действие, завязанное на стейт/UI:
- загрузка данных
- анимации на старте
- однократные запросы, дебаунсы и т.д.
DisposableEffect — imperative-API с жизненным циклом и onDispose
DisposableEffect дает hook типа: onStart/onStop для не-composable API. Позволяет подписаться/инициализироваться и отписаться/очиститься.
@Composable
fun LocationListener(onLocation: (Location) -> Unit) {
val context = LocalContext.current
DisposableEffect(Unit) {
val callback = object : LocationCallback { ... }
val client = LocationServices.getFusedLocationProviderClient(context)
client.requestLocationUpdates(..., callback, Looper.getMainLooper())
onDispose {
// гарантированно вызовется:
// - при уходе из composition
// - при изменении key
client.removeLocationUpdates(callback)
}
}
}
Вызывается один раз на ключ. При изменении ключа сначала вызывается старый onDispose и потом запускается новый блок.
Использовать, когда работаешь с внешними ресурсами:
- listeners:
addCallback/removeCallback - регистрация BroadcastReceiver
- подписка на callback-API, сенсоры, геолокацию
- любые imperative-вещи, где обязательно нужен cleanup
SideEffect — выполнить действие после успешной композиции
SideEffect как бы говорит: «после того, как этот composable успешно вошёл/обновился в composition, выполни вот этот блок один раз». Используется для чистых побочных действий, которые не должны влиять на recomposition и зависят от текущего state.
@Composable
fun LoggingComposable(count: Int) {
Text("Count: $count")
SideEffect {
// вызовется каждый раз после успешной рекомпозиции,
// когда этот composable был частью применённой композиции
Log.d("Compose", "Current count = $count")
}
}
Вызывается после коммита изменений в UI (после applyChanges). При этом, нельзя запускать тяжелые/блокирующие вещи — это не корутина и не suspend.
Используем, когда нужно донести текущее состояние UI наружу:
- логирование
- обновление какого-либо imperative контейнера (например, передать height/width наружу)
- отправить аналитику, изменить флаг, который не влияет на Compose
Если нужен lifecycle-кошерный вход/выход, то берём DisposableEffect. Если нужна корутина, то LaunchedEffect.