Jetpack Compose,使用 NavHost 导航时可变状态会导致无限重组

The*_*bal 5 android kotlin android-jetpack-compose jetpack-compose-navigation compose-recomposition

这是导致无限重组问题的代码

主要活动

class MainActivity : ComponentActivity() {
    
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        setContent {
                val navController = rememberNavController()
                val viewModel : MainViewModel by viewModel()
                val state by viewModel.state.observeAsState()

                NavHost(navController = navController, startDestination = "firstScreen") {
                    composable("firstScreen") { FirstScreen(
                        navigate = {
                        navController.navigate("secondScreen")
                    }, updateState = {
                            viewModel.getState()
                       },
                       state

                    )}

                    composable("secondScreen") { SecondScreen() }
                }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

视图模型

class MainViewModel : ViewModel() {

    //var state = MutableStateFlow(0)

    private val _state = MutableLiveData(0)
    val state: LiveData<Int> = _state

    fun getState()  {
        _state.value = 1
    }
}
Run Code Online (Sandbox Code Playgroud)

第一屏

@Composable
fun FirstScreen(
    navigate: () -> Unit,
    updateState: () -> Unit,
    state: Int?
) {

    Log.e("state",state.toString())

    Button(onClick = {
        updateState()
    }) {
        Text(text = "aaaaaaaa")
    }

    if(state == 1) {
        Log.e("navigate",state.toString())
        navigate()
    }
}
Run Code Online (Sandbox Code Playgroud)

第二屏

@Composable
fun SecondScreen() {...}
Run Code Online (Sandbox Code Playgroud)

按下按钮会更改视图模型中的状态,如果它更改为 1,则会触发导航到第二个屏幕,但第一个屏幕会无限重组并阻止整个过程

编辑

@Composable
fun FirstScreen(
    navigate: () -> Unit,
    updateState: () -> Unit,
    state: Int?
) {


    Log.e("state",state.toString())

    Button(onClick = {
        updateState()
    }) {
        Text(text = "aaaaaaaa")
    }

    LaunchedEffect(state) {

        if (state == 1) {
            Log.e("navigate", state.toString())
            navigate()
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

这解决了问题

z.g*_*g.y 5

这是因为您正在基于条件属性进行导航,该FirstScreen条件属性是可组合项的一部分,并且对该属性的更改超出了范围FirstScreen's,如果该条件属性的值没有更改,则每次更新时它都会NavHost在您的casestate保留1并将始终执行其块。

if(state == 1) {
    ...
    navigate() // navigation
}
Run Code Online (Sandbox Code Playgroud)

您的经历可以通过以下事件来总结:

  • Navhost 配置FirstScreenSecondScreen(初始NavHost composition
  • FirstScreen观察一个state值为0
  • state1单击按钮后变为
  • FirstScreen re-composes,满足条件( ),执行当前时间state==1的导航1st
  • 导航主机re-composes
  • FirstScreen's state剩余,1仍满足条件( ),暂时再次state==1执行导航2nd
  • 导航主机re-composes
  • FirstScreen's state仍为1,满足条件( state==1),再次执行本次3rd导航
  • 并且循环永远不会结束..

根据官方文档

您应该仅将 navigator() 作为回调的一部分调用,而不是作为可组合项本身的一部分,以避免在每次重组时调用 navigator()。

我建议将其视为navigation一次性事件,在内部进行LaunchedEffect并从发射中观察SharedFlow。以下是解决您问题的简短解决方法。

有一堂密封课UiEvent

sealed class UiEvent {
    data class Navigate(val params: Any?): UiEvent()
}
Run Code Online (Sandbox Code Playgroud)

ViewModel像这样修改你的

class MainViewModel : ViewModel() {

    ...

    private val _oneTimeEvent = MutableSharedFlow<UiEvent>()
    val oneTimeEvent = _oneTimeEvent.asSharedFlow()

    ...

    fun navigate()  {
        if (_state.value == 1) {
            viewModelScope.launch {
                _oneTimeEvent.emit(UiEvent.Navigate(1))
            }
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

,然后通过LaunchedEffect在你的FirstScreen

@Composable
fun FirstScreen(
    navigate: () -> Unit,
    ..
) {
    ...
    ...
    
    LaunchedEffect(Unit) {
        mainViewModel.oneTimeEvent.collectLatest { uiEvent ->
            when (uiEvent) {
                is UiEvent.Navigate -> {
                    navigate()
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里查看我的答案