Jetpack Compose Navigation бесконечно загружает экран

я пытаюсь реализовать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()
        }
    }
}

Это ViewModel:

      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... но он продолжает выдаваться после загрузки следующего экрана, что вызывает бесконечную перезагрузку экрана. Что нужно сделать, чтобы избежать этого?

1 ответ

Я повторно реализовал ваш опубликованный код с двумя экранами иSettingScreenи удалил часть класса и его использования.

Проблема в вашем составном, а не вStateFlowэмиссия.

У вас есть это

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

это происходитobservedв одном из ваших блоков, который выполняет обратный вызов.

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

Когда ваша функция вызывается

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

он будет обновлятьuiState, установив его значение в , наблюдаемое с помощью , удовлетворяет блоку, а затем запускает обратный вызов для перехода к следующему экрану.

Теперь на основе официальных документов ,

Вы должны вызывать navigation() только как часть обратного вызова, а не как часть самого компонуемого, чтобы избежать вызова навигации() при каждой рекомпозиции.

но в вашем случае,navigationзапускается наблюдаемым , аmutableStateявляется частью вашего компонуемого.

Похоже, когдаnavControllerвыполняет навигацию и являетсяComposable

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

он выполнитre-composition, из-за этого он снова вызовет (HomeScreen неre-composed, его состояние остается прежним) и посколькуHomeScreen's UiStateзначение по-прежнему равно , оно удовлетворяетwhenblock, снова запускает обратный вызов для навигации иNavHost re-composes, то создается бесконечный цикл .

Что я сделал (и это очень уродливо ), я создал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впоследствии, предотвращая цикл.

Я с трудом могу угадать вашу структуру реализации компоновки, но я обычно не смешиваю свои одноразовые действия событий и 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))
        }
    }

и наблюдать за ним вHomeScreenтаким образом черезLaunchedEffect, запуская в нем навигацию без привязки обратного вызова к какому-либо наблюдаемому состоянию.

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

Этот подход не вводит бесконечный цикл навигации, и больше не нужна грязная логическая проверка.

Также посмотрите этот пост SO, похожий на ваш случай