Как правильно сохранять состояние компонентов в Jetpack Compose

Когда мы разрабатываем приложения с использованием Jetpack Compose, особенно для мобильных устройств, где часто происходят изменения конфигурации (например, поворот экрана), важно правильно сохранять состояние компонентов. В Android классические подходы использовали методы вроде onSaveInstanceState() и ViewModel, чтобы сохранять состояние UI, но в Compose есть более удобные и эффективные инструменты для этих целей.

Почему важно сохранять состояние?

Когда пользователь взаимодействует с приложением, его действия, такие как ввод текста, нажатия кнопок, выбор переключателей и прочее, должны быть сохранены при изменении конфигурации устройства (например, поворот экрана) или при выходе из приложения. Без сохранения состояния приложение будет сбрасываться к начальному состоянию, что ухудшает пользовательский опыт.

Способы сохранения состояния в Compose

  1. remember и rememberSaveable
  2. ViewModel
  3. mapSaver и listSaver

1. remember и rememberSaveable

Compose предлагает функцию remember, которая используется для запоминания значения между пересозданиями UI. Однако, она не сохраняет данные при изменении конфигурации. Для этого предназначена функция rememberSaveable, которая сохраняет состояние даже при изменении конфигурации (например, поворот экрана).

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

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

В этом примере значение count запоминается при пересоздании UI (например, из-за изменения данных), но не сохраняется при изменении конфигурации.

@Composable
fun Counter() {
    var count by rememberSaveable { mutableStateOf(0) }

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

Теперь, если повернуть экран, значение count останется таким, каким оно было до изменения конфигурации.

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

  • Когда нужно сохранять простое состояние компонента между изменениями конфигурации, такими как текстовые поля, счётчики или переключатели.

2. Сохранение состояния с помощью ViewModel

Для более сложных сценариев, где состояние должно быть связано с данными или бизнес-логикой, лучше использовать ViewModel. Состояние, управляемое через ViewModel, сохраняется даже при уничтожении и пересоздании Activity или Fragment.

class CounterViewModel : ViewModel() {
    var count by mutableStateOf(0)
        private set

    fun increment() {
        count++
    }
}

@Composable
fun Counter(viewModel: CounterViewModel = viewModel()) {
    Button(onClick = { viewModel.increment() }) {
        Text(text = "Count: ${viewModel.count}")
    }
}

Здесь состояние count управляется через ViewModel, что гарантирует его сохранение при любых изменениях конфигурации.

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

  • Когда состояние связано с бизнес-логикой или внешними данными, и его нужно сохранять не только при изменении конфигурации, но и между различными экранами.
  • Когда требуется разделение UI и логики обработки данных.

3. mapSaver и listSaver

Когда нужно сохранить более сложные типы данных (например, объекты), которые не могут быть сохранены напрямую через rememberSaveable, можно использовать mapSaver или listSaver.

data class User(val name: String, val age: Int)

val userSaver = mapSaver(
    save = { mapOf("name" to it.name, "age" to it.age) },
    restore = { User(it["name"] as String, it["age"] as Int) }
)

@Composable
fun UserProfile() {
    val user = rememberSaveable(stateSaver = userSaver) { User("John", 30) }

    Column {
        Text(text = "Name: ${user.name}")
        Text(text = "Age: ${user.age}")
    }
}

В этом примере используется mapSaver для сохранения и восстановления объекта User. Мы сохраняем значения полей объекта в карту, а затем восстанавливаем их при необходимости.

Когда использовать mapSaver или listSaver?

  • Когда нужно сохранять сложные объекты (например, классы данных) между изменениями конфигурации.
  • Когда нужно иметь полный контроль над тем, как данные сохраняются и восстанавливаются.