Чем 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.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.