`rememberSaveable`但是在内存中而不是在磁盘上(类似于`viewModel()`)

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

`rememberSaveable` but in-memory instead of on-disk (like `viewModel()`)

问题

In Jetpack Compose,有没有像rememberSaveable一样的东西,但不会保存到磁盘,就像viewModel()一样?目前我有这个:

  1. class MyViewModel : ViewModel() {
  2. var initialized: Boolean = false
  3. var needsToBeInitialized = mutableStateOf<Boolean?>(null)
  4. }
  5. @Composable
  6. fun HelloWorld() {
  7. val model: MyViewModel = viewModel()
  8. // this needs to survive device rotation
  9. var needsToBeInitialized by model.needsToBeInitialized
  10. if (!model.initialized) {
  11. model.needsToBeInitialized.value = getBooleanOrNullSomehow()
  12. model.initialized = true
  13. }
  14. }

使用rememberSaveable只需一行代码,所以我想知道是否有可能像viewModel()一样一行代码实现,以在内存中存储数据。

英文:

In Jetpack Compose, is there something like rememberSaveable but does not save to disk so it works like viewModel()? Currently I'm having this:

  1. class MyViewModel : ViewModel() {
  2. var initialized: Boolean = false
  3. var needsToBeInitialized = mutableStateOf&lt;Boolean?&gt;(null)
  4. }
  5. @Composable
  6. func HelloWorld() {
  7. val model: MyViewModel = viewModel()
  8. // this needs to survive device rotation
  9. var needsToBeInitialized by model.needsToBeInitialized
  10. if (!model.initialized) {
  11. model.needsToBeInitialized.value = getBooleanOrNullSomehow()
  12. model.initialized = true
  13. }
  14. }

With rememberSaveable it's just a nice one-liner, so I was thinking if it was possible to achieve something like viewModel() in just a one line as well, so it stores data in memory instead.

答案1

得分: 1

你可以通过创建一个自定义的rememberInMemory函数来实现你描述的功能,该函数使用一个“通用”ViewModel类,在内存中保存分配给它的任何状态。

  1. /**
  2. * 记住由[init]生成的值。
  3. *
  4. * 它的行为类似于[remember],但存储的值将在配置更改时保留
  5. * (例如,当在Android应用程序中旋转屏幕时)。
  6. * 该值在进程重新创建/死亡时不会存活。
  7. *
  8. * @param dataKey 用作保存值的键的字符串。
  9. * @param inputs 一组输入,当它们中的任何一个发生变化时,将导致状态重置并重新运行[init]
  10. * @param init 用于创建此状态的初始值的工厂函数
  11. * @return 存储在内存中的状态。
  12. */
  13. @Composable
  14. inline fun <reified T> rememberInMemory(
  15. dataKey: String,
  16. vararg inputs: Any?,
  17. crossinline init: @DisallowComposableCalls () -> T,
  18. ): T {
  19. val vm: StateMapViewModel = viewModel()
  20. return remember(*inputs) {
  21. val value = vm.states[dataKey]
  22. ?.takeIf { it.first.contentEquals(inputs) }
  23. ?: Pair(inputs, init()).also {
  24. vm.states[dataKey] = it
  25. }
  26. value.second as T
  27. }
  28. }
英文:

You can achieve what you are describing by creating a custom remember function that is using a "generic" ViewModel class that keeps any state assigned to it in memory.

The "generic" ViewModel and the custom remember function (I called it rememberInMemory)

  1. import androidx.compose.runtime.Composable
  2. import androidx.compose.runtime.DisallowComposableCalls
  3. import androidx.compose.runtime.remember
  4. import androidx.lifecycle.ViewModel
  5. import androidx.lifecycle.viewmodel.compose.viewModel
  6. /**
  7. * A generic [ViewModel] class that will keep any state assigned to it in memory.
  8. */
  9. class StateMapViewModel : ViewModel() {
  10. val states: MutableMap&lt;String, Pair&lt;Array&lt;out Any?&gt;, Any?&gt;&gt; = mutableMapOf()
  11. }
  12. /**
  13. * Remember the value produced by [init].
  14. *
  15. * It behaves similarly to [remember], but the stored value will survive configuration changes
  16. * (for example when the screen is rotated in the Android application).
  17. * The value will not survive process recreation/death.
  18. *
  19. * @param dataKey A string to be used as a key for the saved value.
  20. * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
  21. * reset and [init] to be rerun
  22. * @param init A factory function to create the initial value of this state
  23. * @return The state that is stored in memory.
  24. */
  25. @Composable
  26. inline fun &lt;reified T&gt; rememberInMemory(
  27. dataKey: String,
  28. vararg inputs: Any?,
  29. crossinline init: @DisallowComposableCalls () -&gt; T,
  30. ): T {
  31. val vm: StateMapViewModel = viewModel()
  32. return remember(*inputs) {
  33. val value = vm.states[dataKey]
  34. ?.takeIf { it.first.contentEquals(inputs) }
  35. ?: Pair(inputs, init()).also {
  36. vm.states[dataKey] = it
  37. }
  38. value.second as T
  39. }
  40. }

When calling rememberInMemory a string has to be provided as the dataKey argument. This key will be used to get the current stored value. If the given dataKey does not have a stored value yet, the init lambda will be called to obtain the initial value, which will be stored and returned. The inputs are optional variable length arguments and when any of them have changed it will cause the init lambda to be called again to obtain the new value, which will be stored and returned.

The scope of the data being kept in memory will be the same as the default ViewModel scope, which means this function will work the same as a ViewModel approach would.

When using navigation the StateMapViewModel will be scoped to each NavBackStackEntry. This means that every time when navigating to a new destination, a new StateMapViewModel instance will be created and when navigating back or up, the existing StateMapViewModel instance from the previous NavBackStackEntry will be used. So even with navigation this approach will work the same as would using a ViewModel directly.

If you are using any dependency injection frameworks replace the viewModel() call inside the rememberInMemory function with the correct viewModel()/getViewModel() call from the framework.

<hr>

Below is a demo app showing how this works with data scoped to 3 different navigation destinations and some data scoped to their parent.

Since the demo is using Compose Navigation, you have to add a dependency to your app/build.gradle to try the demo.

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

MainActivity.kt

  1. import android.os.Bundle
  2. import androidx.activity.ComponentActivity
  3. import androidx.activity.compose.setContent
  4. class MainActivity : ComponentActivity() {
  5. override fun onCreate(savedInstanceState: Bundle?) {
  6. super.onCreate(savedInstanceState)
  7. setContent {
  8. Demo()
  9. }
  10. }
  11. }

Demo.kt

  1. import androidx.compose.foundation.background
  2. import androidx.compose.foundation.layout.Arrangement
  3. import androidx.compose.foundation.layout.Column
  4. import androidx.compose.foundation.layout.ColumnScope
  5. import androidx.compose.foundation.layout.Row
  6. import androidx.compose.foundation.layout.padding
  7. import androidx.compose.foundation.shape.RoundedCornerShape
  8. import androidx.compose.material.icons.Icons
  9. import androidx.compose.material.icons.filled.KeyboardArrowDown
  10. import androidx.compose.material.icons.filled.KeyboardArrowUp
  11. import androidx.compose.material3.Button
  12. import androidx.compose.material3.Divider
  13. import androidx.compose.material3.Icon
  14. import androidx.compose.material3.IconButton
  15. import androidx.compose.material3.IconButtonDefaults
  16. import androidx.compose.material3.MaterialTheme
  17. import androidx.compose.material3.Surface
  18. import androidx.compose.material3.Text
  19. import androidx.compose.runtime.Composable
  20. import androidx.compose.runtime.LaunchedEffect
  21. import androidx.compose.runtime.MutableState
  22. import androidx.compose.runtime.getValue
  23. import androidx.compose.runtime.mutableStateOf
  24. import androidx.compose.runtime.setValue
  25. import androidx.compose.ui.Alignment
  26. import androidx.compose.ui.Modifier
  27. import androidx.compose.ui.graphics.Color
  28. import androidx.compose.ui.tooling.preview.Preview
  29. import androidx.compose.ui.unit.Dp
  30. import androidx.compose.ui.unit.dp
  31. import androidx.navigation.compose.NavHost
  32. import androidx.navigation.compose.composable
  33. import androidx.navigation.compose.currentBackStackEntryAsState
  34. import androidx.navigation.compose.rememberNavController
  35. import kotlinx.coroutines.delay
  36. @Preview(showBackground = true)
  37. @Composable
  38. fun Demo() {
  39. @Composable
  40. fun Surface(content: @Composable ColumnScope.() -&gt; Unit) {
  41. Surface(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
  42. Column(modifier = Modifier.padding(8.dp)) {
  43. content()
  44. }
  45. }
  46. }
  47. @Composable
  48. fun Counter(text: String, counterState: MutableState&lt;Int&gt;) {
  49. var counter by counterState
  50. Row(
  51. horizontalArrangement = Arrangement.spacedBy(4.dp),
  52. verticalAlignment = Alignment.CenterVertically
  53. ) {
  54. val modifier = Modifier
  55. .padding(2.dp)
  56. .background(
  57. color = Color.Unspecified,
  58. shape = RoundedCornerShape(100)
  59. )
  60. val colors = IconButtonDefaults.iconButtonColors(
  61. containerColor = MaterialTheme.colorScheme.primary,
  62. contentColor = MaterialTheme.colorScheme.onPrimary
  63. )
  64. IconButton(onClick = { counter-- }, modifier, colors = colors) {
  65. Icon(Icons.Filled.KeyboardArrowDown, contentDescription = &quot;Decrement&quot;)
  66. }
  67. IconButton(onClick = { counter++ }, modifier, colors = colors) {
  68. Icon(Icons.Filled.KeyboardArrowUp, contentDescription = &quot;Increment&quot;)
  69. }
  70. Text(text = &quot;$text: $counter&quot;)
  71. }
  72. }
  73. val navController = rememberNavController()
  74. var countdown by rememberInMemory(&quot;Countdown&quot;) { mutableStateOf(0) }
  75. LaunchedEffect(Unit) {
  76. while (true) {
  77. countdown = 9
  78. while (countdown &gt; 0) {
  79. delay(1000)
  80. countdown--
  81. }
  82. delay(1000)
  83. }
  84. }
  85. val parentCounter = rememberInMemory(&quot;Counter&quot;, countdown &gt; 0) { mutableStateOf(0) }
  86. var previousCounter by rememberInMemory(&quot;Previous Counter&quot;) {
  87. mutableStateOf(parentCounter)
  88. }
  89. Column {
  90. Surface {
  91. Text(&quot;Countdown: $countdown seconds until parent counter resets&quot;)
  92. Counter(&quot;Parent counter&quot;, parentCounter)
  93. Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
  94. val route = navController.currentBackStackEntryAsState().value?.destination?.route
  95. Button(onClick = { navController.navigateUp() }, enabled = route != &quot;countersA&quot;) {
  96. Text(text = &quot;Go back&quot;)
  97. }
  98. when (route) {
  99. &quot;countersA&quot; -&gt; Button(onClick = { navController.navigate(&quot;countersB&quot;) }) {
  100. Text(text = &quot;Show Counters B&quot;)
  101. }
  102. &quot;countersB&quot; -&gt; Button(onClick = { navController.navigate(&quot;countersC&quot;) }) {
  103. Text(text = &quot;Show Counters C&quot;)
  104. }
  105. }
  106. }
  107. }
  108. Divider(thickness = Dp.Hairline)
  109. NavHost(navController = navController, startDestination = &quot;countersA&quot;) {
  110. composable(&quot;countersA&quot;) {
  111. Surface {
  112. val counterA = rememberInMemory(&quot;Counter&quot;) { mutableStateOf(-1) }
  113. LaunchedEffect(Unit) {
  114. previousCounter = counterA
  115. }
  116. Counter(&quot;Counter A&quot;, counterA)
  117. Counter(&quot;Parent Counter&quot;, parentCounter)
  118. }
  119. }
  120. composable(&quot;countersB&quot;) {
  121. Surface {
  122. val counterB = rememberInMemory(&quot;Counter&quot;) { mutableStateOf(-2) }
  123. LaunchedEffect(Unit) {
  124. previousCounter = counterB
  125. }
  126. Counter(&quot;Counter B&quot;, counterB)
  127. Counter(&quot;Previous Counter&quot;, rememberInMemory(&quot;Previous Counter&quot;) { previousCounter })
  128. Counter(&quot;Parent Counter&quot;, parentCounter)
  129. }
  130. }
  131. composable(&quot;countersC&quot;) {
  132. Surface {
  133. val counterC = rememberInMemory(&quot;Counter&quot;) { mutableStateOf(-3) }
  134. LaunchedEffect(Unit) {
  135. previousCounter = counterC
  136. }
  137. Counter(&quot;Counter C&quot;, counterC)
  138. Counter(&quot;Previous Counter&quot;, rememberInMemory(&quot;Previous Counter&quot;) { previousCounter })
  139. Counter(&quot;Parent Counter&quot;, parentCounter)
  140. }
  141. }
  142. }
  143. }
  144. }

答案2

得分: 0

I have improved @Ma3x's answer. This uses DisposableEffect as in rememberSaveable, so dataKey should be unnecessary now.

  1. package com.example.stackoverflow
  2. import androidx.compose.runtime.Composable
  3. import androidx.compose.runtime.DisallowComposableCalls
  4. import androidx.compose.runtime.DisposableEffect
  5. import androidx.compose.runtime.currentCompositeKeyHash
  6. import androidx.compose.runtime.remember
  7. import androidx.compose.runtime.rememberUpdatedState
  8. import androidx.lifecycle.ViewModel
  9. import androidx.lifecycle.viewmodel.compose.viewModel
  10. /**
  11. * A generic [ViewModel] class that will keep any state assigned to it in memory.
  12. */
  13. class StateMapViewModel : ViewModel() {
  14. val states: MutableMap<Int, ArrayDeque<Any>> = mutableMapOf()
  15. }
  16. /**
  17. * Remember the value produced by [init].
  18. *
  19. * It behaves similarly to [remember], but the stored value will survive configuration changes
  20. * (for example when the screen is rotated in the Android application).
  21. * The value will not survive process recreation.
  22. *
  23. * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
  24. * reset and [init] to be rerun
  25. * @param init A factory function to create the initial value of this state
  26. */
  27. @Composable
  28. inline fun <reified T : Any> rememberInMemory(
  29. vararg inputs: Any?,
  30. crossinline init: @DisallowComposableCalls () -> T,
  31. ): T {
  32. val vm: StateMapViewModel = viewModel()
  33. val key = currentCompositeKeyHash
  34. val value = remember(*inputs) {
  35. val states = vm.states[key] ?: ArrayDeque<Any>().also { vm.states[key] = it }
  36. states.removeFirstOrNull() as T? ?: init()
  37. }
  38. val valueState = rememberUpdatedState(value)
  39. DisposableEffect(key) {
  40. onDispose {
  41. vm.states[key]?.addFirst(valueState.value)
  42. }
  43. }
  44. return value
  45. }
英文:

I have improved @Ma3x's answer. This uses DisposableEffect as in rememberSaveable, so dataKey should be unnecessary now.

  1. package com.example.stackoverflow
  2. import androidx.compose.runtime.Composable
  3. import androidx.compose.runtime.DisallowComposableCalls
  4. import androidx.compose.runtime.DisposableEffect
  5. import androidx.compose.runtime.currentCompositeKeyHash
  6. import androidx.compose.runtime.remember
  7. import androidx.compose.runtime.rememberUpdatedState
  8. import androidx.lifecycle.ViewModel
  9. import androidx.lifecycle.viewmodel.compose.viewModel
  10. /**
  11. * A generic [ViewModel] class that will keep any state assigned to it in memory.
  12. */
  13. class StateMapViewModel : ViewModel() {
  14. val states: MutableMap&lt;Int, ArrayDeque&lt;Any&gt;&gt; = mutableMapOf()
  15. }
  16. /**
  17. * Remember the value produced by [init].
  18. *
  19. * It behaves similarly to [remember], but the stored value will survive configuration changes
  20. * (for example when the screen is rotated in the Android application).
  21. * The value will not survive process recreation.
  22. *
  23. * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
  24. * reset and [init] to be rerun
  25. * @param init A factory function to create the initial value of this state
  26. */
  27. @Composable
  28. inline fun &lt;reified T : Any&gt; rememberInMemory(
  29. vararg inputs: Any?,
  30. crossinline init: @DisallowComposableCalls () -&gt; T,
  31. ): T {
  32. val vm: StateMapViewModel = viewModel()
  33. val key = currentCompositeKeyHash
  34. val value = remember(*inputs) {
  35. val states = vm.states[key] ?: ArrayDeque&lt;Any&gt;().also { vm.states[key] = it }
  36. states.removeFirstOrNull() as T? ?: init()
  37. }
  38. val valueState = rememberUpdatedState(value)
  39. DisposableEffect(key) {
  40. onDispose {
  41. vm.states[key]?.addFirst(valueState.value)
  42. }
  43. }
  44. return value
  45. }

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

发表评论

匿名网友

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

确定