You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

弹出导航栈后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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 01:54:57