Reputation: 75
Here is the code that causes the infinite recomposition problem
MainActivity
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() }
}
}
}
}
ViewModel
class MainViewModel : ViewModel() {
//var state = MutableStateFlow(0)
private val _state = MutableLiveData(0)
val state: LiveData<Int> = _state
fun getState() {
_state.value = 1
}
}
First Screen
@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()
}
}
Second Screen
@Composable
fun SecondScreen() {...}
Pressing the button changes the state in the view model and in reaction if it changes to 1 it triggers navigation to the second screen but the first screen is infinitely recomposed and blocks the whole process
Edit
@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()
}
}
}
this solved the problem
Upvotes: 5
Views: 2377
Reputation: 6207
It's because you are navigating based on a conditional property which is part of your FirstScreen
composable and changes to that property are outside of the FirstScreen's
scope, if that conditional property's value doesn't change, it will always evaluate its block every time the NavHost
updates, in your case state
remains 1
and will always executes its block.
if(state == 1) {
...
navigate() // navigation
}
What you experience can be summarized by the events broken down below:
FirstScreen
and SecondScreen
(initial NavHost
composition
)FirstScreen
observes an integer state
with a value of 0
state
becomes 1
after you click the buttonFirstScreen
re-composes
, satisfies the condition (state==1
), executes navigation for the 1st
timere-composes
FirstScreen's
state
remains 1
, still satisfies the condition (state==1
), executes navigation again for the 2nd
timere-composes
FirstScreen's
state
remains 1
, satisfies the condition (state==1
), executes navigation again for the 3rd
timeBased on the official Docs,
You should only call navigate() as part of a callback and not as part of your composable itself, to avoid calling navigate() on every recomposition.
I would advice considering navigation
as a one-time event, doing it inside LaunchedEffect
and observed from a SharedFlow
emission. Below is a short workaround to your problem.
Have a sealed class UiEvent
,
sealed class UiEvent {
data class Navigate(val params: Any?): UiEvent()
}
modify your ViewModel
like this
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))
}
}
}
}
, then observe it via LaunchedEffect
in your FirstScreen
@Composable
fun FirstScreen(
navigate: () -> Unit,
..
) {
...
...
LaunchedEffect(Unit) {
mainViewModel.oneTimeEvent.collectLatest { uiEvent ->
when (uiEvent) {
is UiEvent.Navigate -> {
navigate()
}
}
}
}
}
Please see my answer here
Upvotes: 6