Объясни жизненный цикл Composable-функции. Когда происходит initial composition, recomposition, когда функция уходит из композиции?

Давайте разложим это без магии: у любой @Composable по сути три состояния жизни:

  1. Вошла в композицию (initial composition)
  2. Живёт и время от времени пересоздаётся (recomposition)
  3. Вышла из композиции (disposed)

Compose-рантайм не даёт onCreate/onDestroy, но даёт правила, по которым вызывается код и срабатывают эффекты.

1. Initial composition — первый заход функции в композицию

Когда происходит:

  • setContent { MyScreen() } впервые
  • Внутри другой composable впервые появился вызов Child()
  • Поменялся if/when/navigation так, что участок дерева стал новым
@Composable
fun Screen() {
    if (showDetails) {
        Details() // <= впервые вызвали для этого места в дереве
    }
}

Что реально происходит при initial composition:

  • Compose идёт сверху вниз по дереву @Composable вызовов.
  • Для каждого вызова:
    • Создаёт записи в slot table (структура, где хранится дерево UI + remember-слоты и т.д.)
    • Выполняет remember { ... } => кладёт значения в слоты
    • Запускает сайд-эффекты: LaunchedEffect, DisposableEffect, SideEffect и т.п.
    • Создаёт/меняет нужные LayoutNode/DrawNode (то, что реально рисуется).
  • Пример:
@Composable
fun Counter() {
    println("Counter composed") // <= будет напечатано на initial composition и на каждой рекомпозиции

    var count by remember { mutableStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count = $count")
    }
}
  • На initial composition:
    • remember { mutableStateOf(0) } выполняется => в slot table кладётся MutableState(0)
    • Button/ Text создаются в дереве

2. Recomposition — повторный вызов функции при изменении состояния

Когда происходит:

  • Ты изменил обсервабельное состояние, прочитанное в composable:
    • mutableStateOf, derivedStateOf
    • collectAsState(), remember { mutableStateOf(...) }, rememberSaveable
  • Внешняя система поменяла State<T>, который читает этот composable.
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    // любое изменение count => запрос на рекомпозицию этой части дерева
    Button(onClick = { count++ }) {
        Text("Count = $count")
    }
}

Когда count++:

  1. MutableState помечает «чтение этого стейта => была такая-то композиционная группа«
  2. При записи: state.value = new => Compose инвалидирует соответствующую группу.
  3. Планировщик вызывает recomposition:
    • Повторно вызывает Counter()
    • Переиспользует уже имеющиеся слоты из slot table
    • Сравнивает параметры, может скипнуть части, где ничего не поменялось

Важно:

  • Рекомпозируется не весь экран, а только поддеревья, которые читали изменившийся стейт.
  • Рекомпозиция может батчиться и откладываться, нет гарантии «каждый change => отдельный проход».
  • Если composable не читает ни одного State, он сам по себе никогда не станет причиной рекомпозиции.

Пример с локальной изоляцией:

@Composable
fun Parent() {
    val color by remember { mutableStateOf(Color.Red) }
    Column {
        Child1(color)      // читает color => будет рекомпозиться
        Child2()           // не читает color => можно скипнуть
    }
}

Compose знает, какие группы зависят от каких State, и дергает только нужные.

3. Leaving composition — когда composable «умирает»

Composable выходит из композиции, когда:

  • Родитель перестал его вызывать (ветка if/when стала другой).
  • Он исчез из списка (LazyColumn с ключами, key {} и т.п.).
  • Навигация ушла на другой экран / setContent показал совсем другое дерево.
@Composable
fun Screen(showDetails: Boolean) {
    if (showDetails) {
        Details()
    }
}

Когда showDetails сменился с true на false:

  • Ветка с Details() больше не вызывается.
  • Для группы Details рантайм:
    • удаляет её из slot table
    • вызывает onDispose у всех DisposableEffect
    • отменяет все LaunchedEffect внутри неё
    • выбрасывает remember-состояние (если это не rememberSaveable с внешним стором)

Пример:

@Composable
fun Details() {
    val state = remember { mutableStateOf(0) }

    DisposableEffect(Unit) {
        println("Details ENTER composition")
        onDispose {
            println("Details LEAVE composition")
        }
    }
}

Вывод будет:

  • при первом появлении: Details ENTER composition
  • при скрытии Details: Details LEAVE composition
  • между ними — только рекомпозиции из-за стейта, без повторного ENTER.

Связка с remember / эффектами

remember

  • На initial composition: блок выполняется, результат кладётся в слот.
  • На recomposition: значение берётся из слота, блок не выполняется.
  • На leaving composition: слот удаляется, значение может быть освобождено GC.

rememberSaveable

  • При leaving composition может сохранить значение через SavedStateRegistry и восстановить при возвращении (напр. смена конфигурации / recreate activity).

LaunchedEffect

  • На initial composition: стартует корутина.
  • При изменении key: старая корутина отменяется, создаётся новая.
  • При leaving composition: корутина отменяется.

DisposableEffect

  • На initial composition: выполняется effect блок, возвращается onDispose.
  • При recomposition:
    • если key не меняется → effect не перезапускается
    • если key меняется → старый onDispose вызывается, effect запускается заново.
  • При leaving composition: вызывается последний onDispose.

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

в

от

Метки: