在 Jetpack Compose Navigation 中共享 viewModel
Sharing viewModel within Jetpack Compose Navigation
谁能建议如何在 Jetpack Compose Navigation 的不同部分共享 ViewModel?
根据文档,viewModel 通常应该使用 activity 范围在不同的组合函数中共享,但如果在导航内部则不会。
这是我要修复的代码。看起来我在导航内的两个部分中获得了两个不同的 viewModels:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationSystem()
}
}
}
@Composable
fun NavigationSystem() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("result") { ResultScreen(navController) }
}
}
@Composable
fun HomeScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel()
var temp by remember { mutableStateOf("") }
val fahrenheit = temp.toIntOrNull() ?: 0
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column {
OutlinedTextField(
value = temp,
onValueChange = { temp = it },
label = { Text("Fahrenheit") },
modifier = Modifier.fillMaxWidth(0.85f)
)
Spacer(modifier = Modifier.padding(top = 16.dp))
Button(onClick = {
Log.d("HomeScreen", fahrenheit.toString())
if (fahrenheit !in 1..160) return@Button
viewModel.onCalculate(fahrenheit)
navController.navigate("result")
}) {
Text("Calculate")
}
}
}
}
@Composable
fun ResultScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel()
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Log.d("ResultScreenDebug", "celsius: ${ viewModel.celsius.value.toString()}")
Text(
viewModel.celsius.value.toString(),
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.padding(top = 24.dp))
Button(onClick = { navController.navigate("home") }) {
Text(text = "Calculate again")
}
}
}
调试日志:
2021-07-27 22:01:52.542 27113-27113/com.example.navigation D/ViewModelDebug: fh: 65, cs: 18, celcius: 18.0
2021-07-27 22:01:52.569 27113-27113/com.example.navigation D/ResultScreenDebug: celsius: 0.0
谢谢!
您可以创建一个 viewModel 并将其传递给
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationSystem()
}
}
}
@Composable
fun NavigationSystem() {
val navController = rememberNavController()
val viewModel: ConversionViewModel = viewModel()
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController, viewModel) }
composable("result") { ResultScreen(navController, viewModel) }
}
}
@Composable
fun HomeScreen(navController: NavController, viewModel: ConversionViewModel) {
var temp by remember { mutableStateOf("") }
val fahrenheit = temp.toIntOrNull() ?: 0
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column {
OutlinedTextField(
value = temp,
onValueChange = { temp = it },
label = { Text("Fahrenheit") },
modifier = Modifier.fillMaxWidth(0.85f)
)
Spacer(modifier = Modifier.padding(top = 16.dp))
Button(onClick = {
Log.d("HomeScreen", fahrenheit.toString())
if (fahrenheit !in 1..160) return@Button
viewModel.onCalculate(fahrenheit)
navController.navigate("result")
}) {
Text("Calculate")
}
}
}
}
@Composable
fun ResultScreen(navController: NavController, viewModel: ConversionViewModel) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Log.d("ResultScreenDebug", "celsius: ${ viewModel.celsius.value.toString()}")
Text(
viewModel.celsius.value.toString(),
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.padding(top = 24.dp))
Button(onClick = { navController.navigate("home") }) {
Text(text = "Calculate again")
}
}
}
考虑将您的 activity 作为 viewModelStoreOwner 参数传递给 viewModel() fun,因为 ComponentActivity 实现了 ViewModelStoreOwner 接口:
val viewModel: ConversionViewModel = viewModel(LocalContext.current as ComponentActivity)
此代码将 return 在您的所有目的地中使用相同的 ConversionViewModel 实例。
我认为比将 ViewModel
范围限定到整个 NavGraph
更好的解决方案是在 Home
路由中构建 ViewModel
,然后从 Result
路由(路由范围):
//extensions
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry?.viewModel(): T? = this?.let {
viewModel(viewModelStoreOwner = it)
}
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
): T {
return androidx.lifecycle.viewmodel.compose.viewModel(
viewModelStoreOwner = viewModelStoreOwner, key = T::class.java.name
)
}
//use-case
@Composable
fun HomeScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel()
...
}
@Composable
fun ResultScreen(navController: NavController) {
val viewModel: ConversionViewModel? = navController.previousBackStackEntry.viewModel()
...
}
但是如果你必须将它的范围限定到整个 NavGraph
,你可以像 @akhris 所说的那样做一些事情,但是在某种程度上你可以将 ViewModelStoreOwner
与 [=20] 分开=]:
//composable store-owner builder
@Composable
fun rememberViewModelStoreOwner(): ViewModelStoreOwner {
val context = LocalContext.current
return remember(context) { context as ViewModelStoreOwner }
}
通过这种方式,您可以将 Activity
与 ViewModelStoreOwner
分离,并可以执行以下操作:
val LocalNavGraphViewModelStoreOwner =
staticCompositionLocalOf<ViewModelStoreOwner> {
TODO("Undefined")
}
@Composable
fun NavigationSystem() {
val navController = rememberNavController()
val vmStoreOwner = rememberViewModelStoreOwner()
CompositionLocalProvider(
LocalNavGraphViewModelStoreOwner provides vmStoreOwner
) {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("result") { ResultScreen(navController) }
}
}
}
@Composable
fun HomeScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel(viewModelStoreOwner = LocalNavGraphViewModelStoreOwner.current)
...
}
@Composable
fun ResultScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel(viewModelStoreOwner = LocalNavGraphViewModelStoreOwner.current)
...
}
这还不够好吗?
获取顶部MainScreen 的sharedViewModel 并显式传递。它似乎不会导致任何内存泄漏。
// MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreen()
}
}
}
@Composable
fun MainScreen(sharedViewModel: SharedViewModel = viewModel()) {
val navController = rememberNavController()
Navigation(navController, sharedViewModel)
}
@Composable
// Navigation.kt
fun Navigation(navController: NavHostController, sharedViewModel: SharedViewModel) {
NavHost(navController, startDestination = "Home") {
composable("Home") {
EpisodeListScreen(navController, sharedViewModel)
}
composable("Login") {
LoginScreen(navController, sharedViewModel)
}
composable("Editor") {
EditorScreen(navController, sharedViewModel)
}
composable("Setting") {
SettingScreen(navController, sharedViewModel)
}
}
}
// EpisodeListScreen.kt
@Composable
fun EpisodeListScreen(
navController: NavController,
sharedViewModel: SharedViewModel = viewModel()
) {
// ...
}
谁能建议如何在 Jetpack Compose Navigation 的不同部分共享 ViewModel?
根据文档,viewModel 通常应该使用 activity 范围在不同的组合函数中共享,但如果在导航内部则不会。
这是我要修复的代码。看起来我在导航内的两个部分中获得了两个不同的 viewModels:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationSystem()
}
}
}
@Composable
fun NavigationSystem() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("result") { ResultScreen(navController) }
}
}
@Composable
fun HomeScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel()
var temp by remember { mutableStateOf("") }
val fahrenheit = temp.toIntOrNull() ?: 0
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column {
OutlinedTextField(
value = temp,
onValueChange = { temp = it },
label = { Text("Fahrenheit") },
modifier = Modifier.fillMaxWidth(0.85f)
)
Spacer(modifier = Modifier.padding(top = 16.dp))
Button(onClick = {
Log.d("HomeScreen", fahrenheit.toString())
if (fahrenheit !in 1..160) return@Button
viewModel.onCalculate(fahrenheit)
navController.navigate("result")
}) {
Text("Calculate")
}
}
}
}
@Composable
fun ResultScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel()
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Log.d("ResultScreenDebug", "celsius: ${ viewModel.celsius.value.toString()}")
Text(
viewModel.celsius.value.toString(),
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.padding(top = 24.dp))
Button(onClick = { navController.navigate("home") }) {
Text(text = "Calculate again")
}
}
}
调试日志:
2021-07-27 22:01:52.542 27113-27113/com.example.navigation D/ViewModelDebug: fh: 65, cs: 18, celcius: 18.0
2021-07-27 22:01:52.569 27113-27113/com.example.navigation D/ResultScreenDebug: celsius: 0.0
谢谢!
您可以创建一个 viewModel 并将其传递给
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationSystem()
}
}
}
@Composable
fun NavigationSystem() {
val navController = rememberNavController()
val viewModel: ConversionViewModel = viewModel()
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController, viewModel) }
composable("result") { ResultScreen(navController, viewModel) }
}
}
@Composable
fun HomeScreen(navController: NavController, viewModel: ConversionViewModel) {
var temp by remember { mutableStateOf("") }
val fahrenheit = temp.toIntOrNull() ?: 0
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column {
OutlinedTextField(
value = temp,
onValueChange = { temp = it },
label = { Text("Fahrenheit") },
modifier = Modifier.fillMaxWidth(0.85f)
)
Spacer(modifier = Modifier.padding(top = 16.dp))
Button(onClick = {
Log.d("HomeScreen", fahrenheit.toString())
if (fahrenheit !in 1..160) return@Button
viewModel.onCalculate(fahrenheit)
navController.navigate("result")
}) {
Text("Calculate")
}
}
}
}
@Composable
fun ResultScreen(navController: NavController, viewModel: ConversionViewModel) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Log.d("ResultScreenDebug", "celsius: ${ viewModel.celsius.value.toString()}")
Text(
viewModel.celsius.value.toString(),
style = MaterialTheme.typography.h6
)
Spacer(modifier = Modifier.padding(top = 24.dp))
Button(onClick = { navController.navigate("home") }) {
Text(text = "Calculate again")
}
}
}
考虑将您的 activity 作为 viewModelStoreOwner 参数传递给 viewModel() fun,因为 ComponentActivity 实现了 ViewModelStoreOwner 接口:
val viewModel: ConversionViewModel = viewModel(LocalContext.current as ComponentActivity)
此代码将 return 在您的所有目的地中使用相同的 ConversionViewModel 实例。
我认为比将 ViewModel
范围限定到整个 NavGraph
更好的解决方案是在 Home
路由中构建 ViewModel
,然后从 Result
路由(路由范围):
//extensions
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry?.viewModel(): T? = this?.let {
viewModel(viewModelStoreOwner = it)
}
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
): T {
return androidx.lifecycle.viewmodel.compose.viewModel(
viewModelStoreOwner = viewModelStoreOwner, key = T::class.java.name
)
}
//use-case
@Composable
fun HomeScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel()
...
}
@Composable
fun ResultScreen(navController: NavController) {
val viewModel: ConversionViewModel? = navController.previousBackStackEntry.viewModel()
...
}
但是如果你必须将它的范围限定到整个 NavGraph
,你可以像 @akhris 所说的那样做一些事情,但是在某种程度上你可以将 ViewModelStoreOwner
与 [=20] 分开=]:
//composable store-owner builder
@Composable
fun rememberViewModelStoreOwner(): ViewModelStoreOwner {
val context = LocalContext.current
return remember(context) { context as ViewModelStoreOwner }
}
通过这种方式,您可以将 Activity
与 ViewModelStoreOwner
分离,并可以执行以下操作:
val LocalNavGraphViewModelStoreOwner =
staticCompositionLocalOf<ViewModelStoreOwner> {
TODO("Undefined")
}
@Composable
fun NavigationSystem() {
val navController = rememberNavController()
val vmStoreOwner = rememberViewModelStoreOwner()
CompositionLocalProvider(
LocalNavGraphViewModelStoreOwner provides vmStoreOwner
) {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("result") { ResultScreen(navController) }
}
}
}
@Composable
fun HomeScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel(viewModelStoreOwner = LocalNavGraphViewModelStoreOwner.current)
...
}
@Composable
fun ResultScreen(navController: NavController) {
val viewModel: ConversionViewModel = viewModel(viewModelStoreOwner = LocalNavGraphViewModelStoreOwner.current)
...
}
这还不够好吗?
获取顶部MainScreen 的sharedViewModel 并显式传递。它似乎不会导致任何内存泄漏。
// MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainScreen()
}
}
}
@Composable
fun MainScreen(sharedViewModel: SharedViewModel = viewModel()) {
val navController = rememberNavController()
Navigation(navController, sharedViewModel)
}
@Composable
// Navigation.kt
fun Navigation(navController: NavHostController, sharedViewModel: SharedViewModel) {
NavHost(navController, startDestination = "Home") {
composable("Home") {
EpisodeListScreen(navController, sharedViewModel)
}
composable("Login") {
LoginScreen(navController, sharedViewModel)
}
composable("Editor") {
EditorScreen(navController, sharedViewModel)
}
composable("Setting") {
SettingScreen(navController, sharedViewModel)
}
}
}
// EpisodeListScreen.kt
@Composable
fun EpisodeListScreen(
navController: NavController,
sharedViewModel: SharedViewModel = viewModel()
) {
// ...
}