【问题标题】:Jetpack Compose Navigation loads screen infinitelyJetpack Compose Navigation 无限加载屏幕
【发布时间】:2022-11-07 16:47:10
【问题描述】:

我正在尝试使用单个活动来实现Navigation,并且 多个Composable 屏幕。

这是我的NavHost

@Composable
@ExperimentalFoundationApi
fun MyNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: String = HOME.route,
    viewModelProvider: ViewModelProvider,
    speech: SpeechHelper
) = NavHost(
    modifier = modifier,
    navController = navController,
    startDestination = startDestination
) {
    composable(route = HOME.route) {
        with(viewModelProvider[HomeViewModel::class.java]) {
            HomeScreen(
                speech = speech,
                viewModel = this,
                modifier = Modifier.onKeyEvent { handleKeyEvent(it, this) }
            ) {
                navController.navigateTo(it)
            }
        }
    }

    composable(route = Destination.VOLUME_SETTINGS.route) {
        VolumeSettingsScreen(
            viewModelProvider[VolumeSettingsViewModel::class.java]
        ) { navController.navigateUp() }
    }
}

fun NavHostController.navigateTo(
    navigateRoute: String,
    willGoBackTo: String = HOME.route
): Unit = navigate(navigateRoute) {
    popUpTo(willGoBackTo) { inclusive = true }
}

我的屏幕如下所示:

@Composable
fun HomeScreen(
    speech: SpeechHelper,
    viewModel: HomeViewModel,
    modifier: Modifier,
    onNavigationRequested: (String) -> Unit
) {

    MyBlindAssistantTheme {
        val requester = remember { FocusRequester() }
        val uiState by viewModel.uiState.collectAsStateWithLifecycle(
            initialValue = UiState.Speak(
                R.string.welcome_
                    .withStrResPlaceholder(R.string.text_home_screen)
                    .toSpeechUiModel()
            )
        )

        uiState?.let {
            when (it) {
                is UiState.Speak -> speech.speak(it.speechUiModel)
                is UiState.SpeakRes -> speech.speak(it.speechResUiModel.speechUiModel())
                is UiState.Navigate -> onNavigationRequested(it.route) 
            }
        }

        Column(
            modifier
                .focusRequester(requester)
                .focusable(true)
                .fillMaxSize()
        ) {
            val rowModifier = Modifier.weight(1f)

            Row(rowModifier) {...}
                   
        }

        LaunchedEffect(Unit) {
            requester.requestFocus()
        }
    }
}

这是视图模型:

class HomeViewModel : ViewModel() {
    private val mutableUiState: MutableStateFlow<UiState?> = MutableStateFlow(null)
    val uiState = mutableUiState.asStateFlow()

    
    fun onNavigateButtonClicked(){
        mutableUiState.tryEmit(Destination.VOLUME_SETTINGS.route.toNavigationState())
    }
}

单击按钮时,将调用 ViewModel 并发出 NavigateUiState ......但在加载下一个屏幕后它会继续发出,这会导致无限屏幕重新加载。应该怎么做才能避免这种情况?

【问题讨论】:

    标签: navigation android-jetpack-compose infinite-loop jetpack-compose-navigation compose-recomposition


    【解决方案1】:

    我用 2 个屏幕重新实现了您发布的代码,HomeScreenSettingScreen,并删除了 UiState 类及其用法的一部分。

    问题在于您的HomeScreen 可组合,而不是StateFlow 发射。

    你有这个mutableState

    val uiState by viewModel.uiState.collectAsStateWithLifecycle(
          initialValue = UiState.Speak
    )
    

    那是在执行navigation 回调的when 块之一中的observed

    uiState?.let {
             when (it) {
                 is UiState.Navigate ->  {
                      onNavigationRequested(it.route)
                 }
                 UiState.Speak -> {
                     Log.d("UiState", "Speaking....")
                }
    }
    

    当您的 ViewModel 函数被调用时

     fun onNavigateButtonClicked(){
            mutableUiState.tryEmit(UiState.Navigate(Destination.SETTINGS_SCREEN.route))
     }
    

    它将更新uiState,将其值设置为Navigate,由HomeScreen 观察,满足when 块,然后触发回调以导航到下一个屏幕。

    现基于官方Docs

    您应该只调用 navigate() 作为回调的一部分,而不是作为一部分 你的可组合本身,以避免在每个 重组。

    但在您的情况下,navigation 由观察到的mutableState 触发,mutableState 是您的HomeScreen 可组合的一部分。

    好像navController 执行导航并且NavHostComposable

    @Composable
    public fun NavHost(
        navController: NavHostController,
        startDestination: String,
        modifier: Modifier = Modifier,
        route: String? = null,
        builder: NavGraphBuilder.() -> Unit
    ) { ... }
    

    它将执行re-composition,因此,它将再次调用HomeScreen(HomeScreen 不是re-composed,它的状态保持不变)并且因为HomeScreen's UiState 值仍设置为@987654353 @,它满足 when 块,再次触发回调导航,和 NavHost re-composes,一个无限循环然后创建。

    我所做的(以及非常丑陋的) 是我在 viewModel 中创建了一个boolean 标志,用它有条件地包装回调,

    uiState?.let {
                when (it) {
                    is UiState.Navigate  ->  {
                        if (!viewModel.navigated) {
                            onNavigationRequested(it.route)
                            viewModel.navigated = true
                        } else {
                            // dirty empty else 
                        }
                    }
                    UiState.Speak -> {
                        Log.d("UiState", "Speaking....")
                    }
                }
            }
    

    然后将其设置为true,防止循环。

    我几乎猜不出你的 compose 实现结构,但我通常不会混合我的一次性事件操作和 UiState,而是有一个单独的 UiEvent 密封类,它将对“一次性”事件进行分组,如下所示:

    • 小吃店
    • 吐司
    • 导航

    并将它们作为SharedFlow 发射,因为这些事件不需要任何初始状态或初始值。

    继续,我创建了这个类

    sealed class UiEvent {
        data class Navigate(val route: String) : UiEvent()
    }
    

    ViewModel 中使用它作为类型(在这种情况下为Navigate),

     private val _event : MutableSharedFlow<UiEvent> = MutableSharedFlow()
     val event = _event.asSharedFlow()
    
     fun onNavigateButtonClicked(){
            viewModelScope.launch {
                _event.emit(UiEvent.Navigate(Destination.SETTINGS_SCREEN.route))
            }
        }
    

    并通过LaunchedEffect 以这种方式在HomeScreen 中观察它,触发其中的导航,而不会将回调绑定到任何观察到的状态。

    LaunchedEffect(Unit) {
            viewModel.event.collectLatest {
                when (it) {
                    is UiEvent.Navigate -> {
                        onNavigationRequested(it.route)
                    }
                }
            }
        }
    

    这种方法不会引入无限导航循环,并且不再需要脏布尔检查。

    也看看这个S.O post, similar to your case

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-01-31
      • 2022-09-28
      • 2023-03-07
      • 2021-06-03
      • 2023-03-28
      • 1970-01-01
      • 2021-12-31
      相关资源
      最近更新 更多