Как работает Side-Effect API? Разница между LaunchedEffect, DisposableEffect, SideEffect?

Идеология 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.


Опубликовано

в

,

от

Метки: