弹出导航栈后Compose无法访问Hilt ViewModel问题求助
问题:登录成功弹出栈后获取共享ViewModel抛出异常
我在Compose项目中使用以下代码实现登录页面,通过共享的AuthViewModel处理登录逻辑,登录成功后导航到主页面并弹出之前的认证栈:
@Composable fun LogInScreen( navController: NavHostController, ) { val backStackEntry = remember(navController.currentBackStackEntry) { navController.getBackStackEntry(MainAppNavOptions.AuthScreen.name) } val viewModel: AuthViewModel = hiltViewModel(backStackEntry) ... LaunchedEffect(key1 = viewModel.isUserSignInState.value){ if (viewModel.isUserSignInState.value){ navController.navigate(MainAppNavOptions.MainScreen.name){ popUpTo(MainAppNavOptions.MainScreen.name){ inclusive = true } } } } ... }
登录成功后,LogInScreen会重新执行,此时认证栈已经被弹出,调用navController.getBackStackEntry(MainAppNavOptions.AuthScreen.name)时抛出如下异常:
java.lang.IllegalArgumentException: No destination with route AuthScreen is on the NavController's back stack. The current destination is Destination(0x532bad98) route=HomeScreen at androidx.navigation.NavController.getBackStackEntry(NavController.kt:2440) at com.elog.findnchat.ui.auth.login.LogInScreenKt.LogInScreen(LogInScreen.kt:55) at com.elog.findnchat.ui.navigation.NavGraphKt$authScreens$1$2.invoke(navGraph.kt:204) at com.elog.findnchat.ui.navigation.NavGraphKt$authScreens$1$2.invoke(navGraph.kt:203) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:138) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.navigation.compose.NavHostKt$NavHost$14$1.invoke(NavHost.kt:320) at androidx.navigation.compose.NavHostKt$NavHost$14$1.invoke(NavHost.kt:318) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228) at androidx.compose.runtime.saveable.SaveableStateHolderImpl.SaveableStateProvider(SaveableStateHolder.kt:84) at androidx.navigation.compose.NavBackStackEntryProviderKt.SaveableStateProvider(NavBackStackEntryProvider.kt:65) at androidx.navigation.compose.NavBackStackEntryProviderKt.access$SaveableStateProvider(NavBackStackEntryProvider.kt:1) at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider$1.invoke(NavBackStackEntryProvider.kt:52) at androidx.navigation.compose.NavBackStackEntryProviderKt$LocalOwnersProvider$1.invoke(NavBackStackEntryProvider.kt:51) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:228) at androidx.navigation.compose.NavBackStackEntryProviderKt.LocalOwnersProvider(NavBackStackEntryProvider.kt:47) at androidx.navigation.compose.NavHostKt$NavHost$14.invoke(NavHost.kt:318) at androidx.navigation.compose.NavHostKt$NavHost$14.invoke(NavHost.kt:302) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:138) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1$4.invoke(AnimatedContent.kt:780) at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1$4.invoke(AnimatedContent.kt:769) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:117) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.animation.AnimatedVisibilityKt.AnimatedEnterExitImpl(AnimatedVisibility.kt:941) at androidx.compose.animation.AnimatedVisibilityKt.AnimatedVisibility(AnimatedVisibility.kt:607) at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1.invoke(AnimatedContent.kt:757) at androidx.compose.animation.AnimatedContentKt$AnimatedContent$6$1.invoke(AnimatedContent.kt:740) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:108) at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35) at androidx.compose.animation.AnimatedContentKt.AnimatedContent(AnimatedContent.kt:794) at androidx.navigation.compose.NavHostKt.NavHost(NavHost.kt:280) at androidx.navigation.compose.NavHostKt$NavHost$16.invoke(Unknown Source:27) at androidx.navigation.compose.NavHostKt$NavHost$16.invoke(Unknown Source:10) at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:169) at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(Composer.kt:2468) 2023-08-28 19:15:51.868 7084-7084 AndroidRuntime com.elog.findnchat E at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(Composer.kt:2737) at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3352) at androidx.compose.runtime.ComposerImpl.recompose$runtime_release(Composer.kt:3303) at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:781) at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1097) at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:124) at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:569) at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$1.invoke(Recomposer.kt:537) at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:41) at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:109) at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41) at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:970) at android.view.Choreographer.doCallbacks(Choreographer.java:796) at android.view.Choreographer.doFrame(Choreographer.java:727) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:957) at android.os.Handler.handleCallback(Handler.java:938) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:223) at android.app.ActivityThread.main(ActivityThread.java:7698) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [androidx.compose.runtime.PausableMonotonicFrameClock@9e62ed5, androidx.compose.ui.platform.MotionDurationScaleImpl@70506ea, StandaloneCoroutine{Cancelling}@83b6ddb, AndroidUiDispatcher@f28b078]
补充说明:
- 不弹出栈时功能正常,但弹出栈是必须的业务需求
- 尝试在Composable开头添加
if (viewModel.isUserSignInState.value) return可以解决,但觉得这不是最优方案,想找更好的解决办法。
解决方案
方案1:使用安全方法获取BackStackEntry
用navController.getBackStackEntryOrNull替代getBackStackEntry,避免直接抛出异常,同时在认证栈不存在时直接终止Composable执行或重新导航:
@Composable fun LogInScreen( navController: NavHostController, ) { val authBackStackEntry = remember(navController.currentBackStackEntry) { navController.getBackStackEntryOrNull(MainAppNavOptions.AuthScreen.name) } // 认证栈不存在时,直接返回或确保导航到主页面 authBackStackEntry ?: run { LaunchedEffect(Unit) { navController.navigate(MainAppNavOptions.MainScreen.name) { popUpTo(MainAppNavOptions.MainScreen.name) { inclusive = true } } } return@LogInScreen } val viewModel: AuthViewModel = hiltViewModel(authBackStackEntry) ... LaunchedEffect(key1 = viewModel.isUserSignInState.value){ if (viewModel.isUserSignInState.value){ navController.navigate(MainAppNavOptions.MainScreen.name){ popUpTo(MainAppNavOptions.MainScreen.name){ inclusive = true } } } } ... }
方案2:将ViewModel生命周期绑定到导航图节点
如果AuthScreen是认证导航图的起始节点,可以在导航图级别定义共享ViewModel,让其生命周期由整个导航图持有,直到认证图被完全弹出:
首先修改导航图定义:
fun authNavGraph(navController: NavHostController) { composable(route = MainAppNavOptions.AuthScreen.name) { // 在这里初始化共享ViewModel,作为整个认证图的ViewModel val viewModel: AuthViewModel = hiltViewModel() AuthScreen(viewModel, navController) } composable(route = MainAppNavOptions.LogInScreen.name) { // 从父导航节点获取共享ViewModel val parentEntry = remember { navController.getBackStackEntry(MainAppNavOptions.AuthScreen.name) } val viewModel: AuthViewModel = hiltViewModel(parentEntry) LogInScreen(viewModel, navController) } }
然后简化LogInScreen,直接接收ViewModel参数:
@Composable fun LogInScreen( viewModel: AuthViewModel, navController: NavHostController, ) { ... LaunchedEffect(key1 = viewModel.isUserSignInState.value){ if (viewModel.isUserSignInState.value){ navController.navigate(MainAppNavOptions.MainScreen.name){ popUpTo(MainAppNavOptions.MainScreen.name){ inclusive = true } } } } ... }
方案3:监听导航栈变化动态处理
通过DisposableEffect监听导航栈的变化,实时更新认证栈的状态,避免无效的ViewModel获取:
@Composable fun LogInScreen( navController: NavHostController, ) { var authBackStackEntry by remember { mutableStateOf<NavBackStackEntry?>(null) } DisposableEffect(navController) { val listener = NavController.OnDestinationChangedListener { _, _, _ -> authBackStackEntry = navController.getBackStackEntryOrNull(MainAppNavOptions.AuthScreen.name) } navController.addOnDestinationChangedListener(listener) onDispose { navController.removeOnDestinationChangedListener(listener) } } // 认证栈不存在时直接返回 authBackStackEntry ?: return val viewModel: AuthViewModel = hiltViewModel(authBackStackEntry) ... LaunchedEffect(key1 = viewModel.isUserSignInState.value){ if (viewModel.isUserSignInState.value){ navController.navigate(MainAppNavOptions.MainScreen.name){ popUpTo(MainAppNavOptions.MainScreen.name){ inclusive = true } } } } ... }
内容的提问来源于stack exchange,提问作者nope
相关产品推荐
相关产品推荐

