在 Jetpack Compose Navigation 中共享 viewModel

问题描述 投票:0回答:5

任何人都可以建议如何在 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")
        }
    }
}

调试日志:

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

谢谢!

android-jetpack-compose android-viewmodel android-jetpack-navigation
5个回答
19
投票

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

val viewModel: ConversionViewModel = viewModel(LocalContext.current as ComponentActivity)

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


17
投票

您可以创建一个 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")
        }
    }
}

8
投票

我认为比将

ViewModel
范围扩展到整个
NavGraph
更好的解决方案是在
ViewModel
路线中构建
Home
,然后从
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
Activity
分开:

//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)
    ...
}

0
投票

一个推荐的方法,如果你想访问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)
            }
        }
    }
}

为了方便起见,您可以使用以下扩展功能,可能会很有用:

@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)
}

然后您可以简单地在可组合路线中调用:

val parentViewModel = backStackEntry.sharedViewModel<ParentViewModel>(navController)

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

希望有帮助!


-3
投票

这还不够好吗?

获取主屏幕顶部的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()
) {
    // ...
}
© www.soinside.com 2019 - 2024. All rights reserved.