在 Jetpack Compose Navigation 中共享 viewModel

Has*_*san 43 android-viewmodel android-jetpack-navigation android-jetpack-compose

谁能建议如何在 Jetpack Compose Navigation 的不同部分中共享 ViewModel?

根据文档,viewModel 通常应该使用活动范围在不同的 compose 函数中共享,但如果在导航内部则不然。

这是我试图修复的代码。看起来我在导航内的两个部分中获得了两个不同的 viewModel:

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")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

调试日志:

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
Run Code Online (Sandbox Code Playgroud)

谢谢!

akh*_*ris 27

考虑将您的 Activity 作为 viewModelStoreOwner 参数传递给 viewModel() fun,因为 ComponentActivity 实现了 ViewModelStoreOwner 接口:

val viewModel: ConversionViewModel = viewModel(LocalContext.current as ComponentActivity)
Run Code Online (Sandbox Code Playgroud)

此代码将在您的所有目标中返回 ConversionViewModel 的相同实例。

  • @ShadeToD 我没有将活动实例传递给 ViewModel 构造函数,而是将其作为 ViewModelStoreOwner 的实例传递给 viewModel() fun 。 (6认同)
  • 将活动传递给视图模型是一个不好的模式。这两者应该始终分开。 (2认同)

Gee*_*ers 21

您可以创建一个 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")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 虽然这个解决方案有效,但似乎[官方文档](https://developer.android.com/jetpack/compose/interop/compose-in-existing-arch)说这种方法是不行的:`你永远不应该将 ViewModel 实例传递给其他可组合项,只传递它们需要的数据以及执行所需逻辑的函数作为参数。 (7认同)

ch4*_*4uw 13

ViewModel我认为比将您的范围限制为整个范围更好的解决方案是在路线中NavGraph构建,然后从路线访问(路线范围):ViewModelHomeResult

//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()
    ...
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您必须将其范围扩展到整个NavGraph,您可以执行@akhris 所说的操作,但可以通过某种方式将 与ViewModelStoreOwner分开Activity

//composable store-owner builder
@Composable
fun rememberViewModelStoreOwner(): ViewModelStoreOwner {
    val context = LocalContext.current
    return remember(context) { context as ViewModelStoreOwner }
}
Run Code Online (Sandbox Code Playgroud)

这样你就可以将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)
    ...
}
Run Code Online (Sandbox Code Playgroud)


Ger*_*eto 7

推荐的方法,如果您想访问导航路线或导航图范围内的 ViewModel(即在导航路线或导航图之间共享),您应该使用:

@Composable 
fun MyApp() {
    val navController = rememberNavController()
    val startRoute = "example"
    val innerStartRoute = "exampleWithRoute"
    NavHost(navController, startDestination = startRoute) {
        navigation(startDestination = innerStartRoute, route = "Parent") {
            composable("exampleWithRoute") { backStackEntry ->
                //IMPORTANT PART: getting the scoped ViewModel reference.

                val parentEntry = remember(backStackEntry) {
                    navController.getBackStackEntry("Parent")
                }
                val parentViewModel = hiltViewModel<ParentViewModel>(parentEntry)

                ExampleWithRouteScreen(parentViewModel)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

为了方便起见,您可以使用以下可能有用的扩展函数:

@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(
    navController: NavController,
): T {
    val navGraphRoute = destination.parent?.route ?: return viewModel()
    val parentEntry  = remember(this){
        navController.getBackStackEntry(navGraphRoute)
    }
    return viewModel(parentEntry)
}
Run Code Online (Sandbox Code Playgroud)

然后你可以简单地在你的可组合路由中调用:

val parentViewModel = backStackEntry.sharedViewModel<ParentViewModel>(navController)
Run Code Online (Sandbox Code Playgroud)

您可能还想观看Philipp Lackner 关于该主题的视频

希望能帮助到你!