kotlin `rememberSaveable`但在内存中而不是在磁盘上(如`viewModel()`)

mzsu5hc0  于 2023-05-01  发布在  Kotlin
关注(0)|答案(2)|浏览(245)

在Jetpack Compose中,是否有类似rememberSaveable的东西,但不保存到磁盘,所以它像viewModel()一样工作?目前我有这个:

class MyViewModel : ViewModel() {
    var initialized: Boolean = false

    var needsToBeInitialized = mutableStateOf<Boolean?>(null)
}

@Composable
func HelloWorld() {
    val model: MyViewModel = viewModel()

    // this needs to survive device rotation
    var needsToBeInitialized by model.needsToBeInitialized

    if (!model.initialized) {
        model.needsToBeInitialized.value = getBooleanOrNullSomehow()

        model.initialized = true
    }
}

对于rememberSaveable,它只是一个很好的一行程序,所以我在想是否可以在一行中实现类似viewModel()的东西,所以它将数据存储在内存中。

vh0rcniy

vh0rcniy1#

您可以通过创建一个自定义的remember函数来实现您所描述的功能,该函数使用一个“泛型”ViewModel类,该类将分配给它的任何状态保存在内存中。
“通用”ViewModel和自定义记住函数(我称之为rememberInMemory

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel

/**
 * A generic [ViewModel] class that will keep any state assigned to it in memory.
 */
class StateMapViewModel : ViewModel() {
    val states: MutableMap<String, Pair<Array<out Any?>, Any?>> = mutableMapOf()
}

/**
 * Remember the value produced by [init].
 *
 * It behaves similarly to [remember], but the stored value will survive configuration changes
 * (for example when the screen is rotated in the Android application).
 * The value will not survive process recreation/death.
 *
 * @param dataKey A string to be used as a key for the saved value.
 * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
 * reset and [init] to be rerun
 * @param init A factory function to create the initial value of this state
 * @return The state that is stored in memory.
 */
@Composable
inline fun <reified T> rememberInMemory(
    dataKey: String,
    vararg inputs: Any?,
    crossinline init: @DisallowComposableCalls () -> T,
): T {
    val vm: StateMapViewModel = viewModel()
    return remember(*inputs) {
        val value = vm.states[dataKey]
            ?.takeIf { it.first.contentEquals(inputs) }
            ?: Pair(inputs, init()).also {
                vm.states[dataKey] = it
            }
        value.second as T
    }
}

当调用rememberInMemory时,必须提供一个字符串作为dataKey参数。此键将用于获取当前存储的值。如果给定的dataKey还没有存储值,init lambda将被调用以获得初始值,该初始值将被存储并返回。inputs是可选的可变长度参数,当它们中的任何一个发生变化时,它将导致init lambda再次被调用以获得新值,该值将被存储并返回。
保存在内存中的数据的作用域将与默认的ViewModel作用域相同,这意味着此函数将与ViewModel方法相同。
当使用导航时,StateMapViewModel将被限制到每个NavBackStackEntry。这意味着每次导航到新的目标时,都会创建一个新的StateMapViewModel示例,而当向后或向上导航时,将使用先前NavBackStackEntry中的现有StateMapViewModel示例。因此,即使使用导航,这种方法也将与直接使用ViewModel相同。
如果您使用的是依赖注入框架,请将rememberInMemory函数中的viewModel()调用替换为框架中正确的viewModel()/getViewModel()调用。
下面是一个演示应用程序,展示了如何处理范围为3个不同导航目的地的数据以及范围为其父级的一些数据。
由于演示使用的是Compose Navigation,因此您必须向app/build.gradle添加依赖项才能尝试该演示。

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

MainActivity.kt

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            Demo()
        }
    }
}

Demo.kt

import androidx.compose.foundation.background
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.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay

@Preview(showBackground = true)
@Composable
fun Demo() {
    @Composable
    fun Surface(content: @Composable ColumnScope.() -> Unit) {
        Surface(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
            Column(modifier = Modifier.padding(8.dp)) {
                content()
            }
        }
    }

    @Composable
    fun Counter(text: String, counterState: MutableState<Int>) {
        var counter by counterState
        Row(
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            val modifier = Modifier
                .padding(2.dp)
                .background(
                    color = Color.Unspecified,
                    shape = RoundedCornerShape(100)
                )
            val colors = IconButtonDefaults.iconButtonColors(
                containerColor = MaterialTheme.colorScheme.primary,
                contentColor = MaterialTheme.colorScheme.onPrimary
            )
            IconButton(onClick = { counter-- }, modifier, colors = colors) {
                Icon(Icons.Filled.KeyboardArrowDown, contentDescription = "Decrement")
            }
            IconButton(onClick = { counter++ }, modifier, colors = colors) {
                Icon(Icons.Filled.KeyboardArrowUp, contentDescription = "Increment")
            }
            Text(text = "$text: $counter")
        }
    }

    val navController = rememberNavController()

    var countdown by rememberInMemory("Countdown") { mutableStateOf(0) }
    LaunchedEffect(Unit) {
        while (true) {
            countdown = 9
            while (countdown > 0) {
                delay(1000)
                countdown--
            }
            delay(1000)
        }
    }

    val parentCounter = rememberInMemory("Counter", countdown > 0) { mutableStateOf(0) }

    var previousCounter by rememberInMemory("Previous Counter") {
        mutableStateOf(parentCounter)
    }

    Column {
        Surface {
            Text("Countdown: $countdown seconds until parent counter resets")

            Counter("Parent counter", parentCounter)

            Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
                val route = navController.currentBackStackEntryAsState().value?.destination?.route

                Button(onClick = { navController.navigateUp() }, enabled = route != "countersA") {
                    Text(text = "Go back")
                }

                when (route) {
                    "countersA" -> Button(onClick = { navController.navigate("countersB") }) {
                        Text(text = "Show Counters B")
                    }
                    "countersB" -> Button(onClick = { navController.navigate("countersC") }) {
                        Text(text = "Show Counters C")
                    }
                }
            }
        }

        Divider(thickness = Dp.Hairline)

        NavHost(navController = navController, startDestination = "countersA") {
            composable("countersA") {
                Surface {
                    val counterA = rememberInMemory("Counter") { mutableStateOf(-1) }
                    LaunchedEffect(Unit) {
                        previousCounter = counterA
                    }

                    Counter("Counter A", counterA)

                    Counter("Parent Counter", parentCounter)
                }
            }
            composable("countersB") {
                Surface {
                    val counterB = rememberInMemory("Counter") { mutableStateOf(-2) }
                    LaunchedEffect(Unit) {
                        previousCounter = counterB
                    }

                    Counter("Counter B", counterB)

                    Counter("Previous Counter", rememberInMemory("Previous Counter") { previousCounter })

                    Counter("Parent Counter", parentCounter)
                }
            }
            composable("countersC") {
                Surface {
                    val counterC = rememberInMemory("Counter") { mutableStateOf(-3) }
                    LaunchedEffect(Unit) {
                        previousCounter = counterC
                    }

                    Counter("Counter C", counterC)

                    Counter("Previous Counter", rememberInMemory("Previous Counter") { previousCounter })

                    Counter("Parent Counter", parentCounter)
                }
            }
        }
    }
}
c90pui9n

c90pui9n2#

我改进了@Ma3x的答案,将dataKey设为可选,并删除了takeIf

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.currentCompositeKeyHash
import androidx.compose.runtime.remember
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel

/**
 * A generic [ViewModel] class that will keep any state assigned to it in memory.
 */
class StateMapViewModel : ViewModel() {
    val states: MutableMap<String, Any> = mutableMapOf()
}

/**
 * Remember the value produced by [init].
 *
 * It behaves similarly to [remember], but the stored value will survive configuration changes
 * (for example when the screen is rotated in the Android application).
 * The value will not survive process recreation.
 *
 * @param inputs A set of inputs such that, when any of them have changed, will cause the state to
 * reset and [init] to be rerun
 * @param key An optional key to be used as a key for the saved value. If not provided we use the
 * automatically generated by the Compose runtime which is unique for the every exact code location
 * in the composition tree
 * @param init A factory function to create the initial value of this state
 */
@Composable
inline fun <reified T : Any> rememberInMemory(
    vararg inputs: Any?,
    key: String? = null,
    crossinline init: @DisallowComposableCalls () -> T,
): T {
    val vm: StateMapViewModel = viewModel()

    val finalKey = if (!key.isNullOrEmpty()) {
        key
    } else {
        currentCompositeKeyHash.toString(36)
    }

    return remember(*inputs) {
        val restored = vm.states[finalKey] as T?

        restored ?: init().also { vm.states[finalKey] = it }
    }
}

相关问题