MeL*_*ine 4 android infinite-loop android-jetpack-compose jetpack-compose-navigation compose-recomposition
我正在尝试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 }
}
Run Code Online (Sandbox Code Playgroud)
我的屏幕看起来像这样:
@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()
}
}
}
Run Code Online (Sandbox Code Playgroud)
这是视图模型:
class HomeViewModel : ViewModel() {
private val mutableUiState: MutableStateFlow<UiState?> = MutableStateFlow(null)
val uiState = mutableUiState.asStateFlow()
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(Destination.VOLUME_SETTINGS.route.toNavigationState())
}
}
Run Code Online (Sandbox Code Playgroud)
单击按钮时,ViewModel
将调用并发出 NavigateUiState...但在加载下一个屏幕后,它会继续发出,这会导致无限的屏幕重新加载。应该怎样做才能避免这种情况?
我用两个屏幕重新实现了您发布的代码,HomeScreen
并SettingScreen
删除了该类的某些部分UiState
及其用法。
问题在于您的HomeScreen
可组合项,而不是发射StateFlow
。
你有这个mutableState
val uiState by viewModel.uiState.collectAsStateWithLifecycle(
initialValue = UiState.Speak
)
Run Code Online (Sandbox Code Playgroud)
那是在执行回调的块observed
之一中。when
navigation
uiState?.let {
when (it) {
is UiState.Navigate -> {
onNavigationRequested(it.route)
}
UiState.Speak -> {
Log.d("UiState", "Speaking....")
}
}
Run Code Online (Sandbox Code Playgroud)
当你的ViewModel
函数被调用时
fun onNavigateButtonClicked(){
mutableUiState.tryEmit(UiState.Navigate(Destination.SETTINGS_SCREEN.route))
}
Run Code Online (Sandbox Code Playgroud)
它将更新uiState
,将其值设置为Navigate
,观察到HomeScreen
,满足when
块,然后触发回调以导航到下一个屏幕。
现在根据官方文档,
您应该仅将 navigator() 作为回调的一部分调用,而不是作为可组合项本身的一部分,以避免在每次重组时调用 navigator()。
但就你而言, 是navigation
由观察到的触发的mutableState
,并且mutableState
是你的一部分HomeScreen
。
似乎当navController
执行导航并且NavHost
成为Composable
@Composable
public fun NavHost(
navController: NavHostController,
startDestination: String,
modifier: Modifier = Modifier,
route: String? = null,
builder: NavGraphBuilder.() -> Unit
) { ... }
Run Code Online (Sandbox Code Playgroud)
它将执行 a re-composition
,因此,它将再次调用HomeScreen
(HomeScreen 不是re-composed
,其状态保持不变),并且由于该HomeScreen's
UiState
值仍设置为Navigate
,它满足when
块,再次触发回调进行导航,并且NavHost
re-composes
,无限循环然后创建
我所做的(而且非常丑陋boolean
)是在 viewModel 中创建了一个标志,用它有条件地包装回调,
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....")
}
}
}
Run Code Online (Sandbox Code Playgroud)
并将其设置为true
之后,防止循环。
我很难猜测你的 compose 实现结构,但我通常不会混合一次性事件操作和 UiState,而是有一个单独的UiEvent
密封类,它将对“一次性”事件进行分组,如下所示:
- 小吃店
- 吐司
- 导航
并将它们作为SharedFlow
排放物排放,因为这些事件不需要任何初始状态或初始值。
继续,我创建了这个类
sealed class UiEvent {
data class Navigate(val route: String) : UiEvent()
}
Run Code Online (Sandbox Code Playgroud)
将其用作类型ViewModel
(Navigate
在本例中),
private val _event : MutableSharedFlow<UiEvent> = MutableSharedFlow()
val event = _event.asSharedFlow()
fun onNavigateButtonClicked(){
viewModelScope.launch {
_event.emit(UiEvent.Navigate(Destination.SETTINGS_SCREEN.route))
}
}
Run Code Online (Sandbox Code Playgroud)
HomeScreen
并通过这种方式观察它LaunchedEffect
,触发其中的导航,而无需将回调绑定到任何观察到的状态。
LaunchedEffect(Unit) {
viewModel.event.collectLatest {
when (it) {
is UiEvent.Navigate -> {
onNavigationRequested(it.route)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
这种方法不会引入无限的导航循环,并且不再需要脏布尔检查。
另请看看这篇帖子,与您的情况类似
归档时间: |
|
查看次数: |
1879 次 |
最近记录: |