Как сохранить состояние пользовательского интерфейса в чипе при использовании навигации в компоновке?

У меня есть чип, который меняет конфигурацию при нажатии. Однако, когда пользователь перемещается назад (popbackstack) или переходит к другому экрану, фишка, по которой щелкнули, переустанавливает свое предыдущее положение. Как я понял, мне нужно будет использовать модель просмотра, чтобы передать состояние чипов и таким образом сохранить его? Я не могу найти способ сохранить «запоминающееся» в модели представления.

Как я могу этого добиться? Ценим любой отзыв!

Мой пример чипа:

@Composable
fun CatsChip() {

    val textChipRememberOneState = rememberSaveable { mutableStateOf(false) }

    TextChip(
        isSelected = textChipRememberOneState.value,
        shape = Shapes(medium = RoundedCornerShape(15.dp)),
        text = "Cats",
        selectedColor = LightGreen,
        onChecked = {
            textChipRememberOneState.value = it
        },
    )
}
1
0
105
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете сохранить состояние в MutableStateFlow в ViewModel.

Чтобы использовать ViewModels в Compose, вам нужно добавить следующую зависимость в ваш файл app/build.gradle

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")

Теперь вы можете использовать функцию viewModel(), чтобы получить экземпляр ViewModel в ваших компонуемых объектах.

С ViewModels вам больше не нужно использовать rememberSaveable, так как состояние будет сохранено в ViewModel, однако, если вы хотите, чтобы состояние сохранялось даже после смерти процесса (а не только при изменении конфигурации), вам нужно сохранить состояние в SavedStateHandle.

Вот пример ViewModel, который только хранит состояние в памяти, но не сохраняет его в SavedStateHandle.

class MemoryOnlyViewModel : ViewModel () {
    val checkedState = MutableStateFlow(false)

    fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
}

Вот пример ViewModel, который сохраняет состояние в SavedStateHandle.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)

    fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)

    companion object {
        private const val CHECKED_STATE_KEY = "checkedState"
    }
}

Тогда использование будет выглядеть так

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
        
@Composable
fun CatsChip() {
    val vm: SavedStateViewModel = viewModel() // or: val vm = viewModel<SavedStateViewModel>()
    val catsChipState by vm.checkedState.collectAsState()

    TextChip(
        isSelected = catsChipState,
        shape = Shapes(medium = RoundedCornerShape(15.dp)),
        text = "Cats",
        selectedColor = LightGreen,
        onChecked = vm::onCheckedChange, // or: onChecked = { vm.onCheckedChange(it) }
    )
}

Дополнительные примеры использования см. также в разделе Бизнес-логика документации Compose State Hoisting


Вот демо Composable, использующее навигацию Compose и демонстрирующее две модели представления сверху, сравнивая его с rememberSaveable, рассматривая их двумя разными способами: с родительским контекстом и с NavBackStackEntry. Это показывает, как разные области действия влияют на жизненный цикл ViewModels.

Требуется зависимость навигации Compose в вашем файле app/build.gradle

implementation("androidx.navigation:navigation-compose:2.5.3")

Вы можете проверить демонстрацию, вызвав Demo() в каком-либо составном контенте вашего приложения. Нажмите кнопки для навигации и посмотрите, как изменится стопка. ViewModel, а также rememberSaveable, которые относятся к родительскому контексту, будут сохранять состояние все время, тогда как те, которые относятся к каждому NavBackStackEntry, будут сохранять состояние только для своих собственных пунктов назначения навигации, которые можно увидеть при переходе назад. Также состояние, сохраненное в MemoryOnlyViewModels, не переживет смерть процесса, что можно проверить следующим образом:

  1. Отправьте приложение в фоновый режим, нажав кнопку «Домой» (но не закрывайте его в переключателе приложений)
  2. Завершите процесс, выполнив следующую команду на вкладке Терминал IDE с именем пакета вашего приложения.
adb shell am kill <package_name>
  1. Снова откройте приложение из переключателя приложений.

Если вы правильно выполнили шаги и сумели убить и восстановить процесс таким образом, то вы должны заметить, что только MemoryOnlyViewModels потеряли/сбросили свое состояние.

Вот весь демонстрационный код. Просто скопируйте и вставьте в новый файл Kotlin и вызовите компонуемый Demo() из компонуемого контента.

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

class MemoryOnlyViewModel : ViewModel () {
    val checkedState = MutableStateFlow(false)

    fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
}

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
    val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)

    fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)

    companion object {
        private const val CHECKED_STATE_KEY = "checkedState"
    }
}
    
@Composable
fun Demo() {    
    @Composable
    fun SimpleChip(
        text: String,
        isSelected: Boolean,
        onChecked: (Boolean) -> Unit,
    ) {
        Surface(
            onClick = { onChecked(!isSelected) },
            modifier = Modifier.padding(4.dp),
            shape = RoundedCornerShape(16.dp),
            color = if (isSelected) Color(0xFF7986CB) else Color.LightGray,
        ) {
            Row(
                modifier = Modifier.padding(8.dp),
                horizontalArrangement = Arrangement.spacedBy(4.dp),
                verticalAlignment = Alignment.CenterVertically,
            ) {
                Text(text)
                Icon(
                    imageVector = if (isSelected) Icons.Default.Clear else Icons.Default.AddCircle,
                    contentDescription = null,
                )
            }
        }
    }

    // These VMs are scoped to the lifecycle of the parent context (likely a ComponentActivity)
    val parentMemoryOnlyVm: MemoryOnlyViewModel = viewModel()
    val parentSavedStateVm: SavedStateViewModel = viewModel()
    var parentSaveable by rememberSaveable { mutableStateOf(false) }

    @Composable
    @Suppress("UnusedReceiverParameter")
    fun ColumnScope.DemoScreen(text: String) {
        Text(text)

        val parentMemoryOnlyState by parentMemoryOnlyVm.checkedState.collectAsState()

        SimpleChip(text = "MemoryOnly VM (parent scoped)",
            isSelected = parentMemoryOnlyState, onChecked = parentMemoryOnlyVm::onCheckedChange)

        val navMemoryOnlyVm: MemoryOnlyViewModel = viewModel()
        val navMemoryOnlyState by navMemoryOnlyVm.checkedState.collectAsState()

        SimpleChip(text = "MemoryOnly VM (nav scoped)",
            isSelected = navMemoryOnlyState, onChecked = navMemoryOnlyVm::onCheckedChange)


        val parentSavedState by parentSavedStateVm.checkedState.collectAsState()

        SimpleChip(text = "SavedState VM (parent scoped)",
            isSelected = parentSavedState, onChecked = parentSavedStateVm::onCheckedChange)

        val navSavedStateVm: SavedStateViewModel = viewModel()
        val navSavedState by navSavedStateVm.checkedState.collectAsState()

        SimpleChip(text = "SavedState VM (nav scoped)",
            isSelected = navSavedState, onChecked = navSavedStateVm::onCheckedChange)


        SimpleChip(text = "rememberSaveable (parent scoped)",
            isSelected = parentSaveable, onChecked = { parentSaveable = it })

        var navSaveable by rememberSaveable { mutableStateOf(false) }

        SimpleChip(text = "rememberSaveable (nav scoped)",
            isSelected = navSaveable, onChecked = { navSaveable = it })
    }

    val navController = rememberNavController()

    @Composable
    fun BackButton() = Button(onClick = { navController.navigateUp() }) {
        Text("Go back")
    }

    @Composable
    fun NavButton(route: String) = Button(onClick = { navController.navigate(route) }) {
        Text("Navigate to $route")
    }

    Column {
        val currentEntry by navController.currentBackStackEntryAsState()
        val backStack = remember(currentEntry) {
            navController.backQueue
                .mapNotNull { it.destination.route }
                .joinToString(" > ")
        }
        Text(text = "Backstack: $backStack")

        NavHost(navController = navController, startDestination = "start") {
            composable("start") {
                Row { NavButton(route = "A"); NavButton(route = "B") }
            }
            composable("A") {
                Column {
                    Row { BackButton(); NavButton(route = "B") }
                    DemoScreen("Screen A")
                }
            }
            composable("B") {
                Column {
                    Row { BackButton(); NavButton(route = "A") }
                    DemoScreen("Screen B")
                }
            }
        }
    }
}

Привет! Извините за поздние ответы! Я попробовал код, который вы предложили, но я получаю сообщение об ошибке в MemoryOnlyViewModel относительно этого сегмента:

Josef M 13.05.2023 15:30

Ошибка гласит: «Не удается получить доступ к 'backQueue': он невидим (частный в супертипе) в 'NavHostController'" @Ma3x

Josef M 13.05.2023 15:31

@JosefM В демоверсии используется androidx.navigation:navigation-compose:2.5.3, вы добавили его в зависимости, как описано выше? Сама демонстрация не нужна вам для реализации решения в вашем случае. См. первую часть ответа для этого.

Ma3x 16.05.2023 10:40

еще раз извините за небольшую задержку с ответом, теперь есть больше времени, чтобы попытаться решить эту проблему. Да, я добавил его, но ошибка все еще сохраняется. Это потому, что я нацелен на Android 33 или ? @Ma3x

Josef M 20.05.2023 20:53

Я создал видео, чтобы вы тоже могли его посмотреть. Обратите внимание, что я скопировал точный код, как вы написали, в качестве демонстрации: gyazo.com/0903405e964c6ad452317d84e2519c70

Josef M 20.05.2023 21:02

@JosefM Какую версию Compose вы используете? Просто для демонстрации вы можете закомментировать val backstack и Text(text = "Backstack: $backStack"), так как они просто отображают текущее состояние заднего стека. Демонстрация будет работать даже без отображения дополнительной информации.

Ma3x 23.05.2023 11:33

Это действительно решило мою проблему и очень хорошо объяснено! Спасибо за время и энергию, которую вы вложили, желаю вам всего наилучшего, братан! @Ma3x

Josef M 23.05.2023 21:34

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

Josef M 23.05.2023 21:38

Другие вопросы по теме