Jetpack ComposeMiddleTechnical

Чем remember отличается от rememberSaveable?

remember хранит состояние только во время жизни Composition и теряет его при повороте экрана. rememberSaveable дополнительно сериализует состояние в Bundle, сохраняя его при поворотах и смерти процесса.

remember vs rememberSaveable в Jetpack Compose

remember и rememberSaveable — два API для сохранения состояния в Compose, которые решают разные задачи. Понимание разницы между ними критично для правильного управления жизненным циклом состояния.

remember: выживает только при рекомпозиции

remember сохраняет значение в памяти на протяжении жизни Composition. Когда composable покидает экран (Composition уничтожается) — данные теряются. Рекомпозиция сама по себе не сбрасывает remember-значения.

@Composable
fun CounterWithRemember() {
  // Создаётся один раз при входе в Composition
  // Теряется при: повороте экрана, переходе на другой экран,
  // смерти процесса
  var count by remember { mutableStateOf(0) }

  Column {
    Text("Count: $count")
    Button(onClick = { count++ }) { Text("+") }
  }
}

// remember с вычисляемым значением (пересчёт только при изменении ключей)
@Composable
fun ExpensiveCalculation(input: String) {
  val result = remember(input) {
    // Вычисляется заново только когда input изменился
    input.split(",").map { it.trim() }.filter { it.isNotEmpty() }
  }
  LazyColumn {
    items(result) { item -> Text(item) }
  }
}

rememberSaveable: переживает поворот экрана и смерть процесса

rememberSaveable сохраняет состояние в Bundle, аналогично onSaveInstanceState в Activity/Fragment. Данные переживают поворот экрана, перевод приложения в фон и потенциальную смерть процесса (с последующим восстановлением).

@Composable
fun CounterWithSaveable() {
  // Переживает поворот экрана и смерть процесса
  var count by rememberSaveable { mutableStateOf(0) }

  Column {
    Text("Count: $count")
    Button(onClick = { count++ }) { Text("+") }
  }
}

// Для типов, не поддерживаемых Bundle, нужен Saver
@Composable
fun ColorPicker() {
  val colorSaver = Saver<Color, Int>(
    save = { it.toArgb() },
    restore = { Color(it) }
  )
  var selectedColor by rememberSaveable(stateSaver = colorSaver) {
    mutableStateOf(Color.Red)
  }

  Box(
    modifier = Modifier
      .size(100.dp)
      .background(selectedColor)
      .clickable {
        selectedColor = if (selectedColor == Color.Red) Color.Blue else Color.Red
      }
  )
}

// Для data class используйте @Parcelize
@Parcelize
data class UserSelection(
  val name: String,
  val age: Int
) : Parcelable

@Composable
fun UserForm() {
  var selection by rememberSaveable {
    mutableStateOf(UserSelection("", 0))
  }
  // Работает автоматически т.к. Parcelable поддерживается Bundle
}

Таблица сравнения

  • Рекомпозиция: оба сохраняют состояние
  • Поворот экрана: remember теряет, rememberSaveable сохраняет
  • Смерть процесса: remember теряет, rememberSaveable сохраняет
  • Переход на другой экран (backstack): оба теряют при уничтожении Composition*
  • Ограничение: rememberSaveable хранит только Bundle-совместимые типы (или с кастомным Saver)

* Navigation Compose сохраняет состояние вкладок при restoreState = true через NavOptions.

mapSaver и listSaver

// Альтернативные хелперы для создания Saver
data class Point(val x: Int, val y: Int)

val PointSaver = mapSaver(
  save = { mapOf("x" to it.x, "y" to it.y) },
  restore = { Point(it["x"] as Int, it["y"] as Int) }
)

// Или через listSaver
val PointListSaver = listSaver(
  save = { listOf(it.x, it.y) },
  restore = { Point(it[0], it[1]) }
)

@Composable
fun DrawingCanvas() {
  var cursorPosition by rememberSaveable(stateSaver = PointSaver) {
    mutableStateOf(Point(0, 0))
  }
}

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

  • Используйте remember для: дорогостоящих вычислений (regex, форматирование), временного UI-состояния (hover, focused), объектов, которые нельзя сериализовать (корутины, каналы).
  • Используйте rememberSaveable для: полей ввода, позиции скролла, выбранных фильтров, любого состояния, которое пользователь ожидает сохранить при повороте экрана.

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

  • rememberSaveable ограничен размером Bundle (обычно ~500 КБ на весь Bundle). Хранение больших списков или изображений приведёт к TransactionTooLargeException.
  • Объекты, не являющиеся примитивами, String, Parcelable или Serializable, вызовут IllegalArgumentException без кастомного Saver.
  • remember { mutableStateOf(x) } не обновляется при изменении внешнего параметра x — используйте remember(x) { mutableStateOf(x) } или LaunchedEffect(x) { state.value = x }.
  • Состояние в remember/rememberSaveable привязано к позиции в Composition (call site). Если composable вызывается в условии и условие меняется, состояние сбрасывается.
  • rememberSaveable не работает корректно с MutableList или SnapshotStateList без явного Saver — изменения списка не всегда отслеживаются при восстановлении.
  • При использовании ключей в remember(key1, key2) { ... } любое изменение ключа пересоздаёт запомненный объект — не используйте нестабильные объекты как ключи.
  • Состояние из rememberSaveable восстанавливается до первой композиции, поэтому LaunchedEffect(Unit) в том же composable уже увидит восстановленное значение — будьте осторожны с side-effect'ами при инициализации.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics