Compose UI多次重组合及导航起始目标设置过晚问题求助
Jetpack Compose 应用重组合异常与导航起始目标延迟问题
问题描述
我的Jetpack Compose应用存在两个异常:UI发生多次重组合,且闪屏结束后会先短暂显示登录页,约1秒后才跳转到主页。核心需求是启动时检查用户认证状态:已认证则导航至主页,否则进入认证页面。原本通过闪屏的setKeepOnScreenCondition等待viewModel.userState.value从UNKNOWN变为已知状态,但实际未达到预期效果。
相关代码
AppMainActivity.kt
@AndroidEntryPoint class AppMainActivity : ComponentActivity() { private val viewModel: UserStateViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) installSplashScreen().apply { setKeepOnScreenCondition { Log.d("TESTOWO", "ViewModel addr in MainActivity: ${viewModel.hashCode()}") viewModel.userState.value == UserAuthState.UNKNOWN } } setContent { CarsLocalizerTheme { Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background ) { RootNavGraph(navController = rememberNavController(), viewModel = hiltViewModel()) } } } } }
RootNavGraph.kt
@Composable fun RootNavGraph( navController: NavHostController, viewModel: UserStateViewModel, ) { Log.d("TESTOWO", "ViewModel addr in RootNavGraph: ${viewModel.hashCode()}") NavHost( navController = navController, startDestination = if (viewModel.userState.value == UserAuthState.AUTHENTICATED) HOME_ROUTE else AUTH_ROUTE, route = ROOT_ROUTE ) { authNavGraph(navController = navController) composable(route = HOME_ROUTE) { HomeScreen() } } }
UserStateViewModel.kt
@HiltViewModel class UserStateViewModel @Inject constructor(private val repository: UserStateRepository): ViewModel() { private var _userState = mutableStateOf<UserAuthState>(UserAuthState.UNKNOWN) val userState: State<UserAuthState> get() = _userState private var authStateListenerCallback: FirebaseAuth.AuthStateListener = FirebaseAuth.AuthStateListener { _userState.value = if (it.currentUser != null) UserAuthState.AUTHENTICATED else UserAuthState.UNAUTHENTICATED } init { Log.d("TESTOWO", "View model created") startListeningToAuthState() } override fun onCleared() { super.onCleared() stopListeningToAuthState() } private fun startListeningToAuthState() { repository.setupAuthStateListener(authStateListenerCallback) } private fun stopListeningToAuthState() { repository.removeAuthStateListener(authStateListenerCallback) } }
AuthNavGraph.kt
fun NavGraphBuilder.authNavGraph( navController: NavController ) { navigation( route = AUTH_ROUTE, startDestination = Screen.Login.route ) { composable( route = Screen.Login.route ) { LoginScreen(navController = navController, viewModel = hiltViewModel()) } composable( route = Screen.Register.route ) { RegisterScreen(navController = navController, viewModel = hiltViewModel()) } composable( route = Screen.Onboarding.route ) { OnboardingScreen(navController = navController, viewModel = hiltViewModel()) } } }
启动日志
2022-12-08 12:09:15.668 22945-22945 TESTOWO com.example.carslocalizer D View model created 2022-12-08 12:09:15.668 22945-22945 TESTOWO com.example.carslocalizer D ViewModel addr in RootNavGraph: 15070924 2022-12-08 12:09:15.695 22945-22945 TESTOWO com.example.carslocalizer D ViewModel addr in MainActivity: 15070924 2022-12-08 12:09:15.728 22945-22945 TESTOWO com.example.carslocalizer D ViewModel addr in MainActivity: 15070924 2022-12-08 12:09:15.742 22945-22945 TESTOWO com.example.carslocalizer D ViewModel addr in RootNavGraph: 15070924 2022-12-08 12:09:15.747 22945-22945 TESTOWO com.example.carslocalizer D ViewModel addr in MainActivity: 15070924
问题原因与解决方案
1. 多次重组合的原因
- State驱动重组:
userState是MutableState,每次值变化时,所有读取该状态的Composable(如RootNavGraph)都会触发重组。 - 闪屏轮询机制:
setKeepOnScreenCondition会被系统多次轮询检查,导致日志中多次打印MainActivity的ViewModel实例信息。 - ViewModel实例传递冗余:Activity和
RootNavGraph分别通过viewModels()和hiltViewModel()获取实例(虽日志显示为同一实例,但代码逻辑存在冗余)。
2. 导航起始目标延迟的核心原因
NavHost的startDestination仅在初始化时生效,后续userState值变化不会自动更新导航目标。即使闪屏结束时userState已变为AUTHENTICATED,NavHost仍会以初始的AUTH_ROUTE启动,之后重组时startDestination参数变化也不会触发自动导航。
修复方案
(1)统一ViewModel实例
将Activity中获取的ViewModel直接传递给RootNavGraph,避免冗余获取:
// AppMainActivity的setContent部分 RootNavGraph(navController = rememberNavController(), viewModel = viewModel)
(2)监听状态变化手动导航
在RootNavGraph中通过LaunchedEffect监听userState变化,触发手动导航:
@Composable fun RootNavGraph( navController: NavHostController, viewModel: UserStateViewModel, ) { Log.d("TESTOWO", "ViewModel addr in RootNavGraph: ${viewModel.hashCode()}") // 监听认证状态变化,手动跳转目标页面 LaunchedEffect(viewModel.userState) { val targetRoute = if (viewModel.userState.value == UserAuthState.AUTHENTICATED) HOME_ROUTE else AUTH_ROUTE // 避免重复导航 if (navController.currentDestination?.route != targetRoute) { navController.navigate(targetRoute) { popUpTo(ROOT_ROUTE) { inclusive = true } } } } NavHost( navController = navController, startDestination = AUTH_ROUTE, // 初始占位,后续由LaunchedEffect修正 route = ROOT_ROUTE ) { authNavGraph(navController = navController) composable(route = HOME_ROUTE) { HomeScreen() } } }
(3)优化ViewModel状态初始化
主动获取当前用户状态,减少依赖Firebase AuthStateListener的延迟:
// UserStateViewModel的init块 init { Log.d("TESTOWO", "View model created") // 主动获取当前用户状态,避免等待回调 val currentUser = repository.getCurrentUser() _userState.value = if (currentUser != null) UserAuthState.AUTHENTICATED else UserAuthState.UNAUTHENTICATED startListeningToAuthState() }
(4)减少不必要重组
使用derivedStateOf优化状态读取,仅在userState变化时更新目标路由:
@Composable fun RootNavGraph( navController: NavHostController, viewModel: UserStateViewModel, ) { val targetRoute by remember { derivedStateOf { if (viewModel.userState.value == UserAuthState.AUTHENTICATED) HOME_ROUTE else AUTH_ROUTE } } // ... 后续逻辑使用targetRoute }
内容的提问来源于stack exchange,提问作者gawron103
相关产品推荐
相关产品推荐

