Compose中状态变更触发导航回调导致无限调用问题排查
问题描述
在Jetpack Compose实现启动页时,通过API响应更新状态(从Loading切换到Valid/Invalid)触发导航跳转,但出现onValid无限调用、页面持续闪烁的问题。即使添加hasNavigated标记、使用updateIfDifferent避免重复更新状态、尝试LaunchedEffect限制执行次数,问题仍存在。
排查发现:导航图中其他页面使用了自定义Parcelable类型的NavType(用于传递含嵌套对象的数据),注释掉这些自定义NavType后,启动页恢复正常。
核心代码片段
启动页状态定义
@Immutable data class SplashUiModel( var state: SplashLoadingState = SplashLoadingState.InProgress, ) sealed interface SplashLoadingState { object InProgress: SplashLoadingState object Valid: SplashLoadingState object Invalid: SplashLoadingState }
ViewModel状态管理
private val _state: MutableStateFlow<SplashUiModel> = MutableStateFlow(SplashUiModel()) val state = _state.asStateFlow() // 仅在值不同时更新的扩展函数 inline fun <T> MutableStateFlow<T>.updateIfDifferent(function: (T) -> T) { while (true) { val prevValue = value val nextValue = function(prevValue) if (prevValue == nextValue) { return } if (compareAndSet(prevValue, nextValue)) { return } } } // API响应时更新状态 _state.updateIfDifferent { it.copy(state = SplashLoadingState.Invalid) }
Composable状态收集与导航
val state = viewModel.state.collectAsStateWithLifecycle() when(state.value) { SplashLoadingState.Valid -> { listener.onNavigateValid() } SplashLoadingState.Invalid -> { listener.onNavigateInvalid() } SplashLoadingState.InProgress -> { BaseLoadingIndicator() } } // 导航监听逻辑 var hasNavigated by remember { mutableStateOf(false) } val listener = object : SplashScreenListener { override fun onNavigateValid() { if (!hasNavigated) { hasNavigated = true onValid.invoke() // 调用navController.navigate实现跳转 } } override fun onNavigateInvalid() { if (!hasNavigated) { hasNavigated = true onInvalid.invoke() } } }
自定义NavType代码
dialog<ScreenData.RandomScreen>( typeMap = mapOf( typeOf<ParcelableObject>() to CustomNavTypes.parcelableType<ParcelableObject>(), typeOf<ParcelableObject2>() to CustomNavTypes.parcelableType<ParcelableObject2>(), typeOf<ParcelableObject3>() to CustomNavTypes.parcelableType<ParcelableObject3>() ) ) { RandomScreenComposable( viewModel = hiltViewModel(), ) } inline fun <reified T : Parcelable> parcelableType( isNullableAllowed: Boolean = false, json: Json = Json, ) = object : NavType<T>(isNullableAllowed = isNullableAllowed) { override fun get(bundle: Bundle, key: String) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { bundle.getParcelable(key, T::class.java) } else { @Suppress("DEPRECATION") bundle.getParcelable(key) } override fun parseValue(value: String): T = json.decodeFromString(value) override fun serializeAsValue(value: T): String = json.encodeToString(value) override fun put(bundle: Bundle, key: String, value: T) = bundle.putParcelable(key, value) }
原因分析
问题根源在于自定义NavType的创建方式导致导航图频繁重组:
- 每次导航图相关Composable重组时,
typeMap会生成新的Map实例,且parcelableType函数每次调用都会创建新的NavType匿名对象。 - Navigation Compose将
typeMap视为导航图的关键依赖,一旦typeMap或其中的NavType实例变化,就会触发整个导航图重新构建。 - 导航图重构会导致所有关联页面Composable(包括启动页)重新执行,启动页的状态收集逻辑再次触发,即使状态未变化也会重复进入
when分支,进而反复调用导航回调。
解决方案
核心是让typeMap和自定义NavType成为稳定的、不会频繁变化的对象,具体实现方式如下:
1. 缓存自定义NavType实例
修改parcelableType函数,为每种类型缓存NavType实例,避免每次调用生成新对象:
object CustomNavTypes { private val navTypeCache = mutableMapOf<KClass<*>, NavType<*>>() inline fun <reified T : Parcelable> parcelableType( isNullableAllowed: Boolean = false, json: Json = Json, ): NavType<T> { val key = T::class return navTypeCache.getOrPut(key) { object : NavType<T>(isNullableAllowed = isNullableAllowed) { override fun get(bundle: Bundle, key: String) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { bundle.getParcelable(key, T::class.java) } else { @Suppress("DEPRECATION") bundle.getParcelable(key) } override fun parseValue(value: String): T = json.decodeFromString(value) override fun serializeAsValue(value: T): String = json.encodeToString(value) override fun put(bundle: Bundle, key: String, value: T) = bundle.putParcelable(key, value) } } as NavType<T> } }
2. 使用remember稳定typeMap
在导航图构建时,用remember缓存typeMap,避免每次重组生成新的Map实例:
dialog<ScreenData.RandomScreen>( typeMap = remember { mapOf( typeOf<ParcelableObject>() to CustomNavTypes.parcelableType<ParcelableObject>(), typeOf<ParcelableObject2>() to CustomNavTypes.parcelableType<ParcelableObject2>(), typeOf<ParcelableObject3>() to CustomNavTypes.parcelableType<ParcelableObject3>() ) } ) { RandomScreenComposable( viewModel = hiltViewModel(), ) }
3. 优化启动页导航逻辑(可选)
将导航逻辑移至LaunchedEffect中,仅在状态变化时执行一次,进一步避免重复触发:
val state = viewModel.state.collectAsStateWithLifecycle() var hasNavigated by remember { mutableStateOf(false) } LaunchedEffect(state.value) { if (hasNavigated) return@LaunchedEffect when(state.value) { SplashLoadingState.Valid -> { hasNavigated = true onValid.invoke() } SplashLoadingState.Invalid -> { hasNavigated = true onInvalid.invoke() } else -> {} } } // 仅渲染Loading状态,导航逻辑已移至LaunchedEffect if (state.value is SplashLoadingState.InProgress) { BaseLoadingIndicator() }
内容的提问来源于stack exchange,提问作者Martin
相关产品推荐
相关产品推荐

