Привет, Хабр! На связи Алина, старший Android-разработчик в команде Инвестиций компании «Совкомбанк Технологии». Мы разрабатываем, поддерживаем и улучшаем приложение «Совкомбанк Инвестиции».

Если вы хоть раз теряли пользовательский ввод после поворота экрана или возвращались к приложению из фона и обнаруживали пустой экран вместо заполненной формы — вы знаете, о чем пойдет речь. Сохранение состояния в Android — одна из тех тем, которые кажутся простыми до первого бага в проде. Система может убить процесс в любой момент, конфигурация меняется, а пользователь ожидает, что все останется на своих местах.

Ниже расскажу, как паттерн Memento реализуется в Android через CustomView, SavedStateHandle, Compose и навигацию. Основное внимание уделю CustomView — недооцененному способу сохранения состояния, который позволяет держать экраны легкими. View сама знает, что сохранять и как восстанавливать, без необходимости тащить все в Activity или Fragment. Также рассмотрю типичные ошибки, ограничения Bundle и методы тестирования восстановления после process death.

Зачем Android-разработчику Memento

Memento (Снимок) — поведенческий паттерн, который позволяет сохранять и восстанавливать состояние объекта, не раскрывая его внутреннюю реализацию. В классическом виде участвуют 3 роли:

  • Originator — объект, чьё состояние сохраняется и восстанавливается

  • Memento — неизменяемый снимок состояния

  • Caretaker — сохраняет и передаёт снимки, но не интерпретирует их

В Android эта идея не просто «встречается», а системно поддерживается платформой и библиотеками. Главное для нас — научиться осознанно делегировать сохранение состояния тем компонентам, которые лучше всего знают, что именно им нужно сохранять. Особенно это важно для CustomView, где паттерн раскрывается максимально практично: вид сам хранит своё состояние и восстанавливает его, не перегружая Activity/Fragment/ViewModel.

Ниже представлена Android-ориентированная подборка проверенных применений Memento, с акцентом на View и платформенные механизмы.

Критерии, по которым мы называем решение Memento на Android

  • Originator — объект с чётко инкапсулированным состоянием, например, конкретная View.

  • Memento — компактный, неизменяемый переносимый снимок. Обычно это Parcelable или структура, которую в итоге сериализует Bundle. 

  • Caretaker — хранитель снимков, который сохраняет и возвращает их, не зная внутренней структуры. На Android это часто сам фреймворк через SavedStateRegistry, иерархию View или навигацию.

Важно отметить, что в Android-реализации мы часто отступаем от строгой инкапсуляции классического Memento — Bundle и Parcelable структуры обычно публичны. Это компромисс между чистотой паттерна и практичностью платформы

1) CustomView как первоклассный Originator

Самый «взрослый» Android-кейс Memento — когда состояние UI лежит там, где оно возникает, то есть внутри View. Так вы не раздуваете Activity/Fragment и не плодите лишнюю связность.

Базовый шаблон с BaseSavedState

@Parcelize
 data class InputUiState(
     val userInput: String,
     val errorText: String?,
     val enabled: Boolean
 ) : Parcelable
 
 class InputViewSavedState : BaseSavedState {
     val uiState: InputUiState?
     
     constructor(uiState: InputUiState?, superState: Parcelable?) : super(superState) {
         this.uiState = uiState
     }
     
     internal constructor(source: Parcel) : super(source) {
         uiState = source.readParcelableCompat<InputUiState>()
     }
     
     override fun writeToParcel(out: Parcel, flags: Int) {
         super.writeToParcel(out, flags)
         out.writeParcelable(uiState, flags)
     }
     
     companion object CREATOR : Parcelable.Creator<InputViewSavedState> {
         override fun createFromParcel(source: Parcel) = InputViewSavedState(source)
         override fun newArray(size: Int) = arrayOfNulls<InputViewSavedState>(size)
     }
 }
 
 class InputView @JvmOverloads constructor(
     context: Context,
     attrs: AttributeSet? = null,
     defStyleAttr: Int = 0
 ) : FrameLayout(context, attrs, defStyleAttr) {
 
     private var uiState = InputUiState("", errorText = null, enabled = true)
     private val binding = InputBinding.inflate(LayoutInflater.from(context), this, true)
 
     override fun onSaveInstanceState(): Parcelable {
         val superState = super.onSaveInstanceState()
         return InputViewSavedState(uiState, superState)
     }
 
     override fun onRestoreInstanceState(state: Parcelable?) {
         when (state) {
             is InputViewSavedState -> {
                 super.onRestoreInstanceState(state.superState)
                 state.uiState?.let(::applyState)
             }
             else -> super.onRestoreInstanceState(state)
         }
     }
 
     fun applyState(newState: InputUiState) {
         uiState = newState
         binding.inputEditText.setText(newState.userInput)
         binding.inputLayout.isEnabled = newState.enabled
         binding.inputLayout.apply {
             error = newState.errorText?.takeIf { it.isNotBlank() }
             isErrorEnabled = error != null
         }
     }
 
     fun currentState(): InputUiState = uiState
 }
 
 
 // ParcelExtensions.kt
 inline fun <reified T : Parcelable> Parcel.readParcelableCompat(): T? {
     return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
         readParcelable(T::class.java.classLoader, T::class.java)
     } else {
         @Suppress("DEPRECATION")
         readParcelable(T::class.java.classLoader)
     }
 }

Роли:

  • Originator — InputView

  • Memento — InputViewSavedState с InputUiState

  • Caretaker — Android Framework, который вызывает onSaveInstanceState и затем onRestoreInstanceState

Практические советы:

  • Держите снимок компактным и неизменяемым

  • Не храните ссылки на View, Context, слушатели и прочие несериализуемые поля

  • Убедитесь, что у View есть стабильный id — без него фреймворк не сохранит состояние

  • Сохранение работает по умолчанию. Исключение: если родитель вызвал setSaveFromParentEnabled(false), тогда дочерние View не сохранятся, даже с isSaveEnabled = true

Важно: @Parcelize требует плагин kotlin-parcelize, работает только с val-полями, и все поля должны быть Parcelable/примитивами или их коллекциями. При этом @Parcelize не поддерживает интерфейсы и sealed классы с type parameters.

Сохранение состояния без дочерних View Если требуется сохранить только собственное состояние, а не состояние всех дочерних View, можно переопределить методы dispatchSaveInstanceState и dispatchRestoreInstanceState.

override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
     dispatchFreezeSelfOnly(container) // сохраняем состояние только текущей View, не детей
 }
 
 override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
     dispatchThawSelfOnly(container) // восстанавливаем только текущую View
 }

Это полезно для сложных составных компонентов, где вы сами управляете дочерними состояниями и не хотите дублирования.

Предупреждение: после этого дочерние элем��нты не будут сохраняться автоматически — вы обязаны управлять их состоянием вручную , иначе потеряете, например, позицию RecyclerView.

Фильтры со сбросом и включением

@Parcelize
 data class FilterState(
     val query: String = "",
     val categories: Set<String> = emptySet(),
     val onlyFavorites: Boolean = false,
     val enabled: Boolean = true
 ) : Parcelable {
 
     fun reset(): FilterState = FilterState(enabled = enabled)
     fun toggleEnabled(): FilterState = copy(enabled = !enabled)
 }
 
 class FilterPanelSavedState : BaseSavedState {
     val filterState: FilterState?
     
     constructor(filterState: FilterState?, superState: Parcelable?) : super(superState) {
         this.filterState = filterState
     }
     
     internal constructor(source: Parcel) : super(source) {
         filterState = source.readParcelableCompat<FilterState>()
     }
     
     override fun writeToParcel(out: Parcel, flags: Int) {
         super.writeToParcel(out, flags)
         out.writeParcelable(filterState, flags)
     }
     
     companion object CREATOR : Parcelable.Creator<FilterPanelSavedState> {
         override fun createFromParcel(source: Parcel) = FilterPanelSavedState(source)
         override fun newArray(size: Int) = arrayOfNulls<FilterPanelSavedState>(size)
     }
 }
 
 class FilterPanelView @JvmOverloads constructor(
     context: Context, 
     attrs: AttributeSet? = null
 ) : LinearLayout(context, attrs) {
 
     private val binding = ViewFilterPanelBinding.inflate(
         LayoutInflater.from(context), this, true
     )
 
     private var filterState = FilterState()
 
     fun render(state: FilterState) {
         filterState = state
         isEnabled = state.enabled
         binding.queryEdit.setText(state.query)
         binding.categoriesChips.setSelectedCategories(state.categories)
         binding.favoritesSwitch.isChecked = state.onlyFavorites
     }
 
     fun getState(): FilterState = filterState
 
     fun reset() {
         render(filterState.reset())
     }
 
     fun toggleEnabled() {
         render(filterState.toggleEnabled())
     }
 
     override fun onSaveInstanceState(): Parcelable {
         val superState = super.onSaveInstanceState()
         return FilterPanelSavedState(filterState, superState)
     }
 
     override fun onRestoreInstanceState(state: Parcelable?) {
         when (state) {
             is FilterPanelSavedState -> {
                 super.onRestoreInstanceState(state.superState)
                 state.filterState?.let(::render)
             }
             else -> super.onRestoreInstanceState(state)
         }
     }
 }

Итог: Fragment/Activity остаются тонкими. View сама знает, что ей сохранять и как восстанавливать. Это чистое, локализованное применение Memento.

2) Activity/Fragment и onSaveInstanceState

class CounterActivity : AppCompatActivity() {
 
     private lateinit var binding: ActivityCounterBinding
     private var counter = 0
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         binding = ActivityCounterBinding.inflate(layoutInflater)
         setContentView(binding.root)
 
         counter = savedInstanceState?.getInt(KEY_COUNTER) ?: 0
         binding.counterText.text = counter.toString()
 
         binding.incrementButton.setOnClickListener {
             counter++
             binding.counterText.text = counter.toString()
         }
     }
 
     override fun onSaveInstanceState(outState: Bundle) {
         super.onSaveInstanceState(outState)
         outState.putInt(KEY_COUNTER, counter)
     }
 
     private companion object { const val KEY_COUNTER = "counter" }
 }

Роли:

  • Originator: CounterActivity или конкретные её View

  • Memento: часть Bundle

  • Caretaker: Framework, который сериализует и возвращает savedInstanceState

Уточнения:

  • Когда вызывает: при конфигурационных изменениях и когда система может повредить процесс в фоне — фреймворк заранее сохраняет снимок.

  • Важно: при явном закрытии экрана пользователем, например, через вызов метода finish() или нажатие кнопки «Назад», метод onSaveInstanceState не вызывается, так как пользователь намеренно покинул экран и не ожидает его восстановления. Используйте onPause() или onStop() для сохранения бизнес-данных.

  • Что сохранять: только компактное UI-состояние. Большие данные сохраняйте в репозитории или базе данных.

  • Не дублируйте: если View сохраняет себя, не вытаскивайте её внутренности в Activity или Fragment.

Практические ограничения:

  • Bundle должен быть компактным. Храните только UI-состояние. Большие объёмы приведут к ошибкам уровня IPC

  • Bundle имеет жесткое ограничение ~1MB для IPC транзакций. При превышении получите TransactionTooLargeException. На Android 7.0+ строже — рекомендуется ~500KB

  • Сериализуемость обязательна. Избегайте сложных графов объектов.

  • Не храните: Bitmap, Context, View, Listener, большие коллекции, несериализуемые объекты

3) ViewModel с SavedStateHandle

SavedStateHandle — это обёртка над Bundle из Jetpack, которая сохраняет данные и восстанавливает их даже после убийства процесса.

Упрощенный подход

class CounterViewModel(
    private val stateHandle: SavedStateHandle
) : ViewModel() {

    companion object { private const val KEY_COUNTER = "counter" }

    val counter = stateHandle.getStateFlow(KEY_COUNTER, 0)

    fun increment() {
        stateHandle[KEY_COUNTER] = counter.value + 1
    }
}

Примечание: данный подход представляет собой «ослабленный» вариант паттерна Memento. Здесь нет явного создания неизменяемого снимка, состояние изменяется на месте через SavedStateHandle. Это скорее прозрачная персистентность, чем классический паттерн Memento, но с практической точки зрения он реализует суть паттерна — сохранение и восстановление состояния. Более каноничный Memento для ViewModel

Использование во Fragment:

class CounterFragment : Fragment() {
 
     private val viewModel: CounterViewModel by viewModels()
     private lateinit var binding: FragmentCounterBinding
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         binding = FragmentCounterBinding.bind(view)
 
         viewLifecycleOwner.lifecycleScope.launch {
             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 viewModel.counter.collect { state ->
                     binding.counterText.text = state.value.toString()
                 }
             }
         }
 
         binding.incrementButton.setOnClickListener {
             viewModel.increment()
         }
     }
 }

Роли точнее:

  • Originator: CounterViewModel

  • Memento: CounterState, который сериализуется в Bundle. Или в упрощенном варианте запись ключ-значение, которую в итоге сериализует SavedStateRegistry в Bundle

  • Caretaker: SavedStateRegistry/Framework. SavedStateHandle — это прокси, который предоставляет удобный API для работы с SavedStateRegistry

Важно: SavedStateHandle корректно подключается только если ViewModel создан фабрикой, которая «знает» про SavedStateRegistryOwner. Это может быть делегат by viewModels(), Hilt с аннотацией @HiltViewModel или навигационная фабрика. При ручном создании снимок не будет работать.

Практика:

  • Держите в SavedStateHandle только то, что критично для восстановления UI после процесс-килла и поворотов

  • Для сложных типов используйте маппинг к простым сериализуемым

  • Помните об ограничении ~1MB для Bundle

4) Compose: rememberSaveable и Saver как мост к Memento

Базовый случай

@Composable
 fun Counter() {
     var counter by rememberSaveable { mutableIntStateOf(0) }
     Column {
         Text("Counter: $counter")
         Button(onClick = { counter++ }) { Text("Increment") }
     }
 }

Сложный тип через Saver

data class FormState(val text: String, val enabled: Boolean)
 
 private const val KEY_TEXT = "text"
 private const val KEY_ENABLED = "enabled"
 
 val FormStateSaver: Saver<FormState, Bundle> = Saver(
     save = { state ->
         bundleOf(
             KEY_TEXT to state.text,
             KEY_ENABLED to state.enabled
         )
     },
     restore = { bundle ->
         FormState(
             text = bundle.getString(KEY_TEXT).orEmpty(),
             enabled = bundle.getBoolean(KEY_ENABLED, true)
         )
     }
 )
 
 @Composable
 fun Form() {
     var state by rememberSaveable(stateSaver = FormStateSaver) {
         mutableStateOf(FormState("", true))
     }
     TextField(
         value = state.text,
         onValueChange = { state = state.copy(text = it) },
         enabled = state.enabled
     )
 }

Роли:

  • Originator — конкретный composable или его состояние

  • Memento — результат Saver.save, который далее сериализуется в Bundle

  • Caretaker — Compose runtime вместе с SavedStateRegistry

Практика:

  • Поддерживайте снимки компактными

  • Явно создавайте Saver для собственных типов, чтобы контролировать сериализацию

5) Навигация: сохранение back stack

Если вы реализуете собственную навигацию, сохранение стека экранов можно оформить через паттерн Memento.

// Тип маршрута. В реальном проекте лучше выделить собственный тип.
 typealias Route = String
 
 interface StateSavable<T : Parcelable> {
     fun saveState(): T
     fun restoreState(state: T)
 }
 
 class BackStackNavigator(
     initialStack: List<Route> = emptyList()
 ) : StateSavable<BackStackNavigator.SavedState> {
 
     private val stack = ArrayDeque<Route>().apply { addAll(initialStack) }
 
     fun push(route: Route) = stack.addLast(route)
     fun pop(): Route? = stack.removeLastOrNull()
     fun current(): Route? = stack.lastOrNull()
 
     override fun saveState(): SavedState = SavedState(stack.toList())
     override fun restoreState(state: SavedState) {
         stack.clear()
         stack.addAll(state.routes)
     }
 
     @Parcelize
     data class SavedState(val routes: List<Route>) : Parcelable
 }
 
 class NavigatorStateSaver<T : Parcelable>(
     private val handle: SavedStateHandle,
     private val owner: StateSavable<T>,
     private val key: String
 ) {
     fun save() {
         handle[key] = owner.saveState()
     }
 
     fun restore() {
         handle.get<T>(key)?.let(owner::restoreState)
     }
 
     fun clear() {
         handle.remove<T>(key)
     }
 }
  • Originator — BackStackNavigator управляет состоянием и сам умеет сохраняться/восстанавливаться

  • Memento — SavedState содержит сериализуемый снимок, не раскрывая внутреннюю структуру

  • Caretaker — NavigatorStateSaver работает с любыми Originator-объектами через интерфейс StateSavable, не зная их деталей

Если вы используете официальную Navigation, у неё есть собственное сохранение стека и состояния экранов. Дублировать снимки не нужно, иначе возможна рассинхронизация. В любом случае сохраняйте только легкие route-токены, а не данные экрана.

6) Где Memento не нужен: кэши и репозитории

In-memory кэш и репозитории — это про хранение и доступ к данным, а не про снимок инкапсулированного состояния Originator. Хранение временных сущностей не делает их Memento.

// ❌ Это НЕ Memento - это просто кэш

class UserCacheDataSource {
     private val cache = mutableMapOf<String, User>()
     
     fun getUser(id: String): User? = cache[id]
     fun saveUser(user: User) { cache[user.id] = user }
 }

Ограничения Memento в Android

Когда НЕ использовать savedInstanceState:

1. Большие объемы данных

  • Bundle ограничен ~1MB

  • Для списков >100 элементов, изображений → Room/DataStore

2. Сложные графы объектов

  • Циклические ссылки не сериализуются

  • Храните ID, восстанавливайте из репозитория

3. Бизнес-данные

  • Критичные данные → Room (savedInstanceState ненадежен)

  • Теряется при явном finish()

4. Несериализуемые объекты

  • Context, View, Callback, Thread, InputStream

  • Только Parcelable/Serializable примитивы

5. Пересчитываемые данные

  • Производные значения, кэши → пересоздавайте при восстановлении

Быстрый выбор подхода

  • UI-компонент → BaseSavedState

  • Экран (ViewModel) → SavedStateHandle

  • Compose → rememberSaveable

  • Данные >1MB → Room/DataStore

Интеграция: делегируем сохранение состояния View

Идея, которую удобно применять почти в любом проекте: перенести всю логику сохранения UI-состояния в соответствующие компоненты UI.

Пример: экран с фильтрами и результатами

  •   FilterPanelView хранит свой FilterPanelState через собственный SavedState

  • ResultsRecyclerView опирается на свой LayoutManager с сохранением позиции скролла через стандартный механизм View

  •   Fragment минимален, его задача — связать источники данных, а не таскать бандлы

class FiltersFragment : Fragment(R.layout.fragment_filters) {
 
     private val binding by viewBinding(FragmentFiltersBinding::bind)
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 
         // FilterPanelView сама управляет своим состоянием
         binding.filterPanel.setOnApplyClickListener { state ->
             viewModel.applyFilters(state)
         }
 
         // RecyclerView автоматически сохраняет позицию скролла
         viewLifecycleOwner.lifecycleScope.launch { 
             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { 
                 viewModel.results.collect { results -> 
                     binding.resultsRecycler.submitList(results) 
                 }
             }
         }
     
     }
 }

Что получается:

  • Каждая составная View сама является Originator и несёт ответственность за свой снимок

  • Fragment/Activity не разрастаются «знанием» о внутренностях UI-состояния

  • Восстановление состояния после конфигурационных изменений и процесс-килла становится надёжнее и локальнее

Практические правила и подводные камни

  • Размер снимка. Всё, что идёт в Bundle, должно быть небольшим. Данные загружаем заново, а UI-состояние сохраняем компактно.

  • Только сериализуемые типы. Никаких ссылок на Context, View, Listener.

  • Не дублируйте сохранение. Если View уже хранит состояние сама, не пытайтесь ещё раз складывать то же в Activity.onSaveInstanceState.

  • Чётко отделяйте UI-снимок и данные. UI-снимки живут в SavedState, данные живут в репозиториях/БД.

  • Тестируйте цикл поворота и процесс-килл. Прогоняйте сценарии пересоздания, чтобы гарантировать корректное восстановление.

  • Compose. Для кастомных типов всегда задавайте явный Saver, чтобы контролировать формат снимка.

  • CustomView. Если ваш компонент — контейнер, и вы управляете дочерними View сами, используйте dispatchFreezeSelfOnly/dispatchThawSelfOnly.

Процесс-килл: как это работает и как корректно тестировать

Как это работает:

  • Система может убить ваш процесс в фоне, чтобы освободить ресурсы. Перед этим фреймворк стремится сохранить снимки состояний активных компонентов в SavedStateRegistry/Bundle. При последующем возврате в задачу из «Недавних» система воссоздает компоненты и передает сохраненный savedInstanceState.

  • Важно: при явном закрытии экрана пользователем, например, через вызов метода finish() или свайп задачи из «Недавних», сохранение savedInstanceState не гарантировано. Это не инструмент для бизнес-данных.

Что точно не эмулирует процесс-килл адекватно:

  • Опция разработчика Don’t keep activities — она уничтожает активность при уходе с экрана, но это не тождественно убийству процесса системой. Полезно для ловли ошибок жизненного цикла, но не для чистой симуляции процесс-килла.

Рабочие сценарии проверки

  1. Домой → убить процесс → вернуться из «Недавних»

    1. Откройте нужный экран, приведите его в требуемое состояние

    2. Нажмите Home, чтобы уйти на главный экран

    3. Убейте процесс командой: adb shell am kill your.package.name — убивает все фоновые процессы пакета, если они не на переднем плане.

    4. Откройте приложение из «Недавних» — система должна воссоздать процесс и активность, передав savedInstanceState. Проверьте, что состояние восстановилось.

  2. Свернуть → нагрузить память → вернуться

    1. Сверните приложение

    2. Откройте тяжелые приложения, чтобы вытеснить ваш процесс

    3. Вернитесь из «Недавних». Если процесс был выгружен, произойдет восстановление из снимка

  3. Лимит фоновых процессов = 0

    1. В настройках разработчика включите «Лимит фоновых процессов» и установите «Без фоновых процессов»

    2. Переключитесь в другое приложение

    3. На большинстве устройств система будет убивать ваш процесс сразу при уходе в фон, что помогает поймать дефекты восстановления. Поведение может отличаться между версиями Android и прошивками

  4. Force stop - НЕ эквивалент процесс-киллу

    «Settings - Apps - Your app - Force stop» или adb shell am force-stop your.package.nameЭто ближе к полной остановке приложения: снимаются будильники, блокируются широковещательные сообщения до следующего запуска. Возврат из списка «Недавние приложения» часто недоступен или приведет к холодному старту без savedInstanceState. Это тест не на процесс-килл, а на «холодный запуск после остановки». Полезен, но для другой категории сценариев.

Автоматизация тестирования process death:

@RunWith(AndroidJUnit4::class)
 class SaveStateTest {
     
     @get:Rule
     val activityScenario = ActivityScenarioRule(MainActivity::class.java)
     
     @Test
     fun testStateRestorationAfterRecreation() {
         // Arrange: устанавливаем состояние
         onView(withId(R.id.counter_text)).check(matches(withText("0")))
         onView(withId(R.id.increment_button)).perform(click())
         onView(withId(R.id.counter_text)).check(matches(withText("1")))
         
         // Act: эмулируем пересоздание (rotation)
         activityScenario.scenario.recreate()
         
         // Assert: проверяем восстановление
         onView(withId(R.id.counter_text)).check(matches(withText("1")))
     }
 }

Чеклист в конце теста:

  • Возврат из «Недавних» после am kill восстанавливает экран с ожидаемым savedInstanceState

  • CustomView с BaseSavedState вернула свое состояние без участия Activity/Fragment

  • ViewModel восстановил критичные поля из SavedStateHandle

  • Compose компоненты с rememberSaveable восстановили значения

  • Нет крашей из-за classloader или не-saveable типов

Итоги:

  • CustomView как Originator — сильнейший Android-кейс Memento. Вид сам знает, что сохранять и как восстанавливать, а фреймворк выступает Caretaker.

  • Activity/Fragment — используем для небольших UI-снимков через onSaveInstanceState.

  • ViewModel + SavedStateHandle - храните критичные для восстановления UI значения, помня про сериализацию и размер.

  • Compose — rememberSaveable и Saver маппят ваше состояние к сериализуемому снимку.

  • Навигация — делайте явный Parcelable memento стека и восстанавливайте из него.

  • Кэш и репозитории — это не Memento. Не смешивайте роли.

Такой подход снижает связность, делает экраны тоньше и повышает надёжность восстановления. Главное — держать снимки компактными и близкими к месту, где живёт состояние, то есть в самих UI-компонентах.