У меня есть чип, который меняет конфигурацию при нажатии. Однако, когда пользователь перемещается назад (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
},
)
}
Вы можете сохранить состояние в MutableStateFlow
в ViewModel
.
Чтобы использовать ViewModel
s в Compose, вам нужно добавить следующую зависимость в ваш файл app/build.gradle
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
Теперь вы можете использовать функцию viewModel()
, чтобы получить экземпляр ViewModel
в ваших компонуемых объектах.
С ViewModel
s вам больше не нужно использовать 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
. Это показывает, как разные области действия влияют на жизненный цикл ViewModel
s.
Требуется зависимость навигации Compose в вашем файле app/build.gradle
implementation("androidx.navigation:navigation-compose:2.5.3")
Вы можете проверить демонстрацию, вызвав Demo()
в каком-либо составном контенте вашего приложения.
Нажмите кнопки для навигации и посмотрите, как изменится стопка. ViewModel
, а также rememberSaveable
, которые относятся к родительскому контексту, будут сохранять состояние все время, тогда как те, которые относятся к каждому NavBackStackEntry
, будут сохранять состояние только для своих собственных пунктов назначения навигации, которые можно увидеть при переходе назад.
Также состояние, сохраненное в MemoryOnlyViewModel
s, не переживет смерть процесса, что можно проверить следующим образом:
adb shell am kill <package_name>
Если вы правильно выполнили шаги и сумели убить и восстановить процесс таким образом, то вы должны заметить, что только MemoryOnlyViewModel
s потеряли/сбросили свое состояние.
Вот весь демонстрационный код. Просто скопируйте и вставьте в новый файл 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")
}
}
}
}
}
Ошибка гласит: «Не удается получить доступ к 'backQueue': он невидим (частный в супертипе) в 'NavHostController'" @Ma3x
@JosefM В демоверсии используется androidx.navigation:navigation-compose:2.5.3
, вы добавили его в зависимости, как описано выше? Сама демонстрация не нужна вам для реализации решения в вашем случае. См. первую часть ответа для этого.
еще раз извините за небольшую задержку с ответом, теперь есть больше времени, чтобы попытаться решить эту проблему. Да, я добавил его, но ошибка все еще сохраняется. Это потому, что я нацелен на Android 33 или ? @Ma3x
Я создал видео, чтобы вы тоже могли его посмотреть. Обратите внимание, что я скопировал точный код, как вы написали, в качестве демонстрации: gyazo.com/0903405e964c6ad452317d84e2519c70
@JosefM Какую версию Compose вы используете? Просто для демонстрации вы можете закомментировать val backstack
и Text(text = "Backstack: $backStack")
, так как они просто отображают текущее состояние заднего стека. Демонстрация будет работать даже без отображения дополнительной информации.
Это действительно решило мою проблему и очень хорошо объяснено! Спасибо за время и энергию, которую вы вложили, желаю вам всего наилучшего, братан! @Ma3x
На самом деле я перечитал ваш комментарий, и вы правы. Один сохраняет его при изменении навигации и конфигурации, а другой сохраняет его на протяжении всего времени, независимо от навигации!
Привет! Извините за поздние ответы! Я попробовал код, который вы предложили, но я получаю сообщение об ошибке в MemoryOnlyViewModel относительно этого сегмента: