如何在使用Compose中的导航时在芯片内保存UI状态?

huangapple go评论83阅读模式
英文:

How to save UI state within a chip while using navigation in compose?

问题

I have a chip that changes configurations when being clicked on. However, as the user navigates back (popbackstack) or navigates to another screen, the clicked chip re-sets its previous position. As I understood, I will have to use a viewmodel to pass the chips state and in that way save it? I cant find a way though to store the "remembersavable" in a viewmodel.

How can I achieve this? Appreciate any feedback!

My example chip:

  1. @Composable
  2. fun CatsChip() {
  3. val textChipRememberOneState = rememberSaveable { mutableStateOf(false) }
  4. TextChip(
  5. isSelected = textChipRememberOneState.value,
  6. shape = Shapes(medium = RoundedCornerShape(15.dp)),
  7. text = "Cats",
  8. selectedColor = LightGreen,
  9. onChecked = {
  10. textChipRememberOneState.value = it
  11. },
  12. )
  13. }
英文:

I have a chip that changes configurations when being clicked on. However, as the user navigates back (popbackstack) or navigates to another screen, the clicked chip re-sets its previous position. As I understood, I will have to use a viewmodel to pass the chips state and in that way save it? I cant find a way though to store the "remembersavable" in a viewmodel.

How can I achieve this? Appreciate any feedback!

My example chip:

  1. @Composable
  2. fun CatsChip() {
  3. val textChipRememberOneState = rememberSaveable { mutableStateOf(false) }
  4. TextChip(
  5. isSelected = textChipRememberOneState.value,
  6. shape = Shapes(medium = RoundedCornerShape(15.dp)),
  7. text = "Cats",
  8. selectedColor = LightGreen,
  9. onChecked = {
  10. textChipRememberOneState.value = it
  11. },
  12. )
  13. }

答案1

得分: 1

以下是您要翻译的内容:

You can keep the state in a MutableStateFlow in a ViewModel.

To use ViewModels in Compose you need to add the following dependency to your app/build.gradle file

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

Now you can use the viewModel() function to get the ViewModel instance in your composables.

With ViewModels you do not need to use rememberSaveable anymore, since the state will be kept in the ViewModel, however, if you want the state to persist even across process death (not just configuration changes), then you have to save the state in the SavedStateHandle.

Here is an example of a ViewModel that only keeps the state in memory, but does not save it in the SavedStateHandle.

  1. class MemoryOnlyViewModel : ViewModel () {
  2. val checkedState = MutableStateFlow(false)
  3. fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
  4. }

Here is an example of a ViewModel that saves the state in the SavedStateHandle.

  1. class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
  2. val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)
  3. fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)
  4. companion object {
  5. private const val CHECKED_STATE_KEY = "checkedState"
  6. }
  7. }

Then the usage would look like this

  1. import androidx.compose.runtime.getValue
  2. import androidx.compose.runtime.setValue
  3. @Composable
  4. fun CatsChip() {
  5. val vm: SavedStateViewModel = viewModel() // or: val vm = viewModel<SavedStateViewModel>()
  6. val catsChipState by vm.checkedState.collectAsState()
  7. TextChip(
  8. isSelected = catsChipState,
  9. shape = Shapes(medium = RoundedCornerShape(15.dp)),
  10. text = "Cats",
  11. selectedColor = LightGreen,
  12. onChecked = vm::onCheckedChange, // or: onChecked = { vm.onCheckedChange(it) }
  13. )
  14. }

For more use cases see also the Business Logic section of the Compose State Hoisting documentation

Here is a demo Composable using Compose navigation and showcasing the two view models from above,
comparing it with rememberSaveable, scoping them in two different ways, to the parent context and to
the NavBackStackEntry. This shows how different scopes affect the lifecycle of ViewModels.

Requires the Compose navigation dependency in your app/build.gradle file

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

You can check the demo by calling Demo() in some composable content of your app.
Click the buttons to navigate and see how the backstack changes. The ViewModels and also the rememberSaveable that are scoped to the parent context will preserve the state all the time, whereas those that are scoped to each NavBackStackEntry will preserve state only for their own navigation destinations, which can be seen when navigating back.
Also the state saved in the MemoryOnlyViewModels will not survive process death, which you can check in the following way:

  1. Send the application to the background by pressing the Home button (but do not close it in the app switcher)
  2. Kill the process by running the following command in the IDE Terminal tab with the package name of your application
  1. adb shell am kill <package_name>
  1. Open the application again from the app switcher

If you followed the steps correctly and managed to kill and restore the process in this way, then you should notice that only MemoryOnlyViewModels have lost/reset their state.

Here is the whole demo code. Just copy and paste to a new Kotlin file and call the Demo() composable from a composable content.

英文:

You can keep the state in a MutableStateFlow in a ViewModel.

To use ViewModels in Compose you need to add the following dependency to your app/build.gradle file

  1. implementation(&quot;androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1&quot;)

Now you can use the viewModel() function to get the ViewModel instance in your composables.

With ViewModels you do not need to use rememberSaveable anymore, since the state will be kept in
the ViewModel, however, if you want the state to persist even across process death (not just configuration changes), then you have to save the state in the SavedStateHandle.

Here is an example of a ViewModel that only keeps the state in memory, but does not save it in the SavedStateHandle.

  1. class MemoryOnlyViewModel : ViewModel () {
  2. val checkedState = MutableStateFlow(false)
  3. fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
  4. }

Here is an example of a ViewModel that saves the state in the SavedStateHandle.

  1. class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
  2. val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)
  3. fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)
  4. companion object {
  5. private const val CHECKED_STATE_KEY = &quot;checkedState&quot;
  6. }
  7. }

Then the usage would look like this

  1. import androidx.compose.runtime.getValue
  2. import androidx.compose.runtime.setValue
  3. @Composable
  4. fun CatsChip() {
  5. val vm: SavedStateViewModel = viewModel() // or: val vm = viewModel&lt;SavedStateViewModel&gt;()
  6. val catsChipState by vm.checkedState.collectAsState()
  7. TextChip(
  8. isSelected = catsChipState,
  9. shape = Shapes(medium = RoundedCornerShape(15.dp)),
  10. text = &quot;Cats&quot;,
  11. selectedColor = LightGreen,
  12. onChecked = vm::onCheckedChange, // or: onChecked = { vm.onCheckedChange(it) }
  13. )
  14. }

For more use cases see also the Business Logic section of the Compose State Hoisting documentation

<hr>

Here is a demo Composable using Compose navigation and showcasing the two view models from above,
comparing it with rememberSaveable, scoping them in two different ways, to the parent context and to
the NavBackStackEntry. This shows how different scopes affect the lifecycle of ViewModels.

Requires the Compose navigation dependency in your app/build.gradle file

  1. implementation(&quot;androidx.navigation:navigation-compose:2.5.3&quot;)

You can check the demo by calling Demo() in some composable content of your app.
Click the buttons to navigate and see how the backstack changes. The ViewModels and also the rememberSaveable that are scoped to the parent context will preserve the state all the time, whereas those that are scoped to each NavBackStackEntry will preserve state only for their own navigation destinations, which can be seen when navigating back.
Also the state saved in the MemoryOnlyViewModels will not survive process death, which you can check in the following way:

  1. Send the application to the background by pressing the Home button (but do not close it in the app switcher)
  2. Kill the process by running the following command in the IDE Terminal tab with the package name of your application
  1. adb shell am kill &lt;package_name&gt;
  1. Open the application again from the app switcher

If you followed the steps correctly and managed to kill and restore the process in this way, then you should notice that only MemoryOnlyViewModels have lost/reset their state.

Here is the whole demo code. Just copy and paste to a new Kotlin file and call the Demo() composable from a composable content.

  1. import androidx.compose.foundation.layout.Arrangement
  2. import androidx.compose.foundation.layout.Column
  3. import androidx.compose.foundation.layout.ColumnScope
  4. import androidx.compose.foundation.layout.Row
  5. import androidx.compose.foundation.layout.padding
  6. import androidx.compose.foundation.shape.RoundedCornerShape
  7. import androidx.compose.material.icons.Icons
  8. import androidx.compose.material.icons.filled.AddCircle
  9. import androidx.compose.material.icons.filled.Clear
  10. import androidx.compose.material3.Button
  11. import androidx.compose.material3.Icon
  12. import androidx.compose.material3.Surface
  13. import androidx.compose.material3.Text
  14. import androidx.compose.runtime.Composable
  15. import androidx.compose.runtime.collectAsState
  16. import androidx.compose.runtime.getValue
  17. import androidx.compose.runtime.mutableStateOf
  18. import androidx.compose.runtime.remember
  19. import androidx.compose.runtime.saveable.rememberSaveable
  20. import androidx.compose.runtime.setValue
  21. import androidx.compose.ui.Alignment
  22. import androidx.compose.ui.Modifier
  23. import androidx.compose.ui.graphics.Color
  24. import androidx.compose.ui.unit.dp
  25. import androidx.lifecycle.SavedStateHandle
  26. import androidx.lifecycle.ViewModel
  27. import androidx.lifecycle.viewmodel.compose.viewModel
  28. import androidx.navigation.compose.NavHost
  29. import androidx.navigation.compose.composable
  30. import androidx.navigation.compose.currentBackStackEntryAsState
  31. import androidx.navigation.compose.rememberNavController
  32. import kotlinx.coroutines.flow.MutableStateFlow
  33. import kotlinx.coroutines.flow.update
  34. class MemoryOnlyViewModel : ViewModel () {
  35. val checkedState = MutableStateFlow(false)
  36. fun onCheckedChange(isChecked: Boolean) = checkedState.update { isChecked }
  37. }
  38. class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
  39. val checkedState = state.getStateFlow(key = CHECKED_STATE_KEY, initialValue = false)
  40. fun onCheckedChange(isSelected: Boolean) = state.set(key = CHECKED_STATE_KEY, value = isSelected)
  41. companion object {
  42. private const val CHECKED_STATE_KEY = &quot;checkedState&quot;
  43. }
  44. }
  45. @Composable
  46. fun Demo() {
  47. @Composable
  48. fun SimpleChip(
  49. text: String,
  50. isSelected: Boolean,
  51. onChecked: (Boolean) -&gt; Unit,
  52. ) {
  53. Surface(
  54. onClick = { onChecked(!isSelected) },
  55. modifier = Modifier.padding(4.dp),
  56. shape = RoundedCornerShape(16.dp),
  57. color = if (isSelected) Color(0xFF7986CB) else Color.LightGray,
  58. ) {
  59. Row(
  60. modifier = Modifier.padding(8.dp),
  61. horizontalArrangement = Arrangement.spacedBy(4.dp),
  62. verticalAlignment = Alignment.CenterVertically,
  63. ) {
  64. Text(text)
  65. Icon(
  66. imageVector = if (isSelected) Icons.Default.Clear else Icons.Default.AddCircle,
  67. contentDescription = null,
  68. )
  69. }
  70. }
  71. }
  72. // These VMs are scoped to the lifecycle of the parent context (likely a ComponentActivity)
  73. val parentMemoryOnlyVm: MemoryOnlyViewModel = viewModel()
  74. val parentSavedStateVm: SavedStateViewModel = viewModel()
  75. var parentSaveable by rememberSaveable { mutableStateOf(false) }
  76. @Composable
  77. @Suppress(&quot;UnusedReceiverParameter&quot;)
  78. fun ColumnScope.DemoScreen(text: String) {
  79. Text(text)
  80. val parentMemoryOnlyState by parentMemoryOnlyVm.checkedState.collectAsState()
  81. SimpleChip(text = &quot;MemoryOnly VM (parent scoped)&quot;,
  82. isSelected = parentMemoryOnlyState, onChecked = parentMemoryOnlyVm::onCheckedChange)
  83. val navMemoryOnlyVm: MemoryOnlyViewModel = viewModel()
  84. val navMemoryOnlyState by navMemoryOnlyVm.checkedState.collectAsState()
  85. SimpleChip(text = &quot;MemoryOnly VM (nav scoped)&quot;,
  86. isSelected = navMemoryOnlyState, onChecked = navMemoryOnlyVm::onCheckedChange)
  87. val parentSavedState by parentSavedStateVm.checkedState.collectAsState()
  88. SimpleChip(text = &quot;SavedState VM (parent scoped)&quot;,
  89. isSelected = parentSavedState, onChecked = parentSavedStateVm::onCheckedChange)
  90. val navSavedStateVm: SavedStateViewModel = viewModel()
  91. val navSavedState by navSavedStateVm.checkedState.collectAsState()
  92. SimpleChip(text = &quot;SavedState VM (nav scoped)&quot;,
  93. isSelected = navSavedState, onChecked = navSavedStateVm::onCheckedChange)
  94. SimpleChip(text = &quot;rememberSaveable (parent scoped)&quot;,
  95. isSelected = parentSaveable, onChecked = { parentSaveable = it })
  96. var navSaveable by rememberSaveable { mutableStateOf(false) }
  97. SimpleChip(text = &quot;rememberSaveable (nav scoped)&quot;,
  98. isSelected = navSaveable, onChecked = { navSaveable = it })
  99. }
  100. val navController = rememberNavController()
  101. @Composable
  102. fun BackButton() = Button(onClick = { navController.navigateUp() }) {
  103. Text(&quot;Go back&quot;)
  104. }
  105. @Composable
  106. fun NavButton(route: String) = Button(onClick = { navController.navigate(route) }) {
  107. Text(&quot;Navigate to $route&quot;)
  108. }
  109. Column {
  110. val currentEntry by navController.currentBackStackEntryAsState()
  111. val backStack = remember(currentEntry) {
  112. navController.backQueue
  113. .mapNotNull { it.destination.route }
  114. .joinToString(&quot; &gt; &quot;)
  115. }
  116. Text(text = &quot;Backstack: $backStack&quot;)
  117. NavHost(navController = navController, startDestination = &quot;start&quot;) {
  118. composable(&quot;start&quot;) {
  119. Row { NavButton(route = &quot;A&quot;); NavButton(route = &quot;B&quot;) }
  120. }
  121. composable(&quot;A&quot;) {
  122. Column {
  123. Row { BackButton(); NavButton(route = &quot;B&quot;) }
  124. DemoScreen(&quot;Screen A&quot;)
  125. }
  126. }
  127. composable(&quot;B&quot;) {
  128. Column {
  129. Row { BackButton(); NavButton(route = &quot;A&quot;) }
  130. DemoScreen(&quot;Screen B&quot;)
  131. }
  132. }
  133. }
  134. }
  135. }

huangapple
  • 本文由 发表于 2023年4月19日 17:05:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76052665.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定