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
значение по-прежнему равно , оно удовлетворяетwhen
block, снова запускает обратный вызов для навигации и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, похожий на ваш случай