Jetpack ComposeMiddleTechnical

Каков жизненный цикл composable в Jetpack Compose?

Жизненный цикл composable: Enter (появление в дереве), Recompose (перерисовка при изменении State), Leave (удаление из дерева). Не зависит от Android lifecycle. DisposableEffect очищает ресурсы при Leave, LaunchedEffect отменяет корутины.

Жизненный цикл composable в Jetpack Compose

Жизненный цикл composable не совпадает с Android-lifecycle (Activity/Fragment). Он определяется присутствием composable в дереве composition и состоит из трёх этапов: Enter, Recompose, Leave.

Три фазы

  • Enter (Initial Composition). Composable впервые появляется в дереве. Инициализируются все remember-значения, запускаются эффекты с Unit-ключом или первым значением ключа.
  • Recomposition. Один или несколько читаемых State-объектов изменились. Compose вызывает функцию повторно, обновляя только затронутые части дерева. При одинаковых входных данных composable может быть пропущен (skipped).
  • Leave (Disposal). Composable покидает дерево (условие if стало false, навигация назад, родитель удалён). Запускается cleanup всех DisposableEffect-блоков, отменяются все LaunchedEffect-корутины.

Slot Table и идентичность

Compose хранит состояние в Slot Table, индексируя позиции по месту вызова в коде. Composable считается «тем же» между recomposition, если его позиция в дереве не изменилась. При использовании в списках уникальность гарантирует явный key(id) { ... }.

Пример с эффектами жизненного цикла

@Composable
fun LocationTracker(userId: String) {
    val context = LocalContext.current

    // Запускается при Enter, перезапускается при смене userId, отменяется при Leave
    LaunchedEffect(userId) {
        trackLocation(userId) // suspend-функция
    }

    // Подписка на Android LocationManager с cleanup при Leave
    DisposableEffect(Unit) {
        val listener = LocationListener { location ->
            // обработка
        }
        val manager = context.getSystemService(LocationManager::class.java)
        manager.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,
            1000L, 10f, listener
        )
        onDispose {
            manager.removeUpdates(listener) // Cleanup при Leave
        }
    }

    Text("Tracking $userId")
}

Связь с Android lifecycle

Composable и Activity/Fragment живут независимо, но их нужно явно связывать через LocalLifecycleOwner. Например, чтобы паузировать подписку при onStop:

@Composable
fun LifecycleAwareComponent() {
    val lifecycle = LocalLifecycleOwner.current.lifecycle

    DisposableEffect(lifecycle) {
        val observer = LifecycleEventObserver { _, event ->
            when (event) {
                Lifecycle.Event.ON_RESUME -> startWork()
                Lifecycle.Event.ON_PAUSE  -> stopWork()
                else -> Unit
            }
        }
        lifecycle.addObserver(observer)
        onDispose { lifecycle.removeObserver(observer) }
    }
}

Подводные камни

  • DisposableEffect без cleanup. Если зарегистрировать listener в DisposableEffect, но не удалить его в onDispose { }, он переживёт composable и вызовет утечку памяти или краш.
  • LaunchedEffect с неправильным ключом. LaunchedEffect(Unit) запустится один раз при Enter. LaunchedEffect(userId) — при каждом изменении userId. Если ключ — нестабильный объект, эффект будет перезапускаться при каждой recomposition.
  • Параллельная recomposition. В Compose 1.5+ recomposition может выполняться параллельно (если включена reuseActivation). Код внутри composable должен быть thread-safe или работать только через State.
  • remember не привязан к Navigation backstack. remember-состояние живёт столько, сколько composable находится в composition. При навигации назад с popBackStack экран пересоздаётся и состояние сбрасывается, если не использовать rememberSaveable или ViewModel.
  • Анимации удерживают composable в composition. AnimatedVisibility и AnimatedContent держат уходящий composable в дереве до конца exit-анимации. DisposableEffect-cleanup отложен соответственно.
  • Порядок эффектов не гарантирован между composable. Если два composable запускают LaunchedEffect, нет гарантии очерёдности их старта. Не полагайтесь на межкомпонентный порядок инициализации.
  • rememberCoroutineScope vs LaunchedEffect. rememberCoroutineScope() возвращает scope, привязанный к composition. Корутины в нём не отменяются при recomposition, только при Leave — это может вызвать race condition при быстрых обновлениях state.
  • ViewModel не заменяет DisposableEffect. ViewModel переживает смену конфигурации, но не отменяет Android-подписки (BroadcastReceiver, LocationListener). Cleanup платформенных ресурсов всегда должен быть в composable через DisposableEffect.

Common mistakes

  • Объяснять «жизненный цикл composable» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Если хранить внешние listeners без DisposableEffect, они переживают composable и создают leak.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «жизненный цикл composable» своими словами и связывает ее с кодом.
  • Называет механизм: Lifecycle определяется не Activity callbacks, а presence in composition, keys и state reads; effects получают cleanup при leaving.
  • Видит production-последствие: Если хранить внешние listeners без DisposableEffect, они переживают composable и создают leak.

Sources

Related topics