Pablo
07/03/2025, 2:54 PMval uiState by vm.uiState.collectAsStateWithLifecycle()
and inside that screen I have a composable which iterates one list of elements contained on that uistate:
ListPane(items = uiState.itemsMap,
I want a recomposition if the user changes the order of the map (I have a button for that), but it's a map, and unfortunately if you change the map order, JVM will consider the same instance of the previous unordered map, so recomposition is not dispatched.
But I noticed that if I have a different variable in the uistate, for example, a count which is increased each time the map is ordered... then, suddenly the screen starts recomposing. How it's possible? if I am not reading that count value anywhere, is just stored in the uistate, but is not used or accesed.Seri
07/03/2025, 3:20 PMuiState.count
variable increments, the value of your entire State is changed so recomposition is triggered. From this point, the Compose compiler will call any children functions if any of their parameters are detected to change.Seri
07/03/2025, 3:22 PMMap
isn't causing recomposition by itself because it's considered Unstable by the Compose compiler. There's no way for the compiler to tell if the underlying data has changed: https://developer.android.com/develop/ui/compose/performance/stabilitySeri
07/03/2025, 3:26 PMSnapshotStateMap
class, generated by mutableStateMapOf(...)
https://developer.android.com/reference/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMapPablo
07/03/2025, 3:43 PMPablo
07/03/2025, 3:43 PMChrimaeon
07/03/2025, 3:45 PMPablo
07/03/2025, 3:50 PM_uiState.update { it.copy(loading = true) }
_uiState.update { it.copy(map = newMap) }
_uiState.update { it.copy(loading = false) }
I supposed that modifying "loading" whould dispatch a recomposition like the count did, but not. why the count do it and loading not ?Seri
07/03/2025, 4:34 PMuiState
. Could you try reducing it into a smaller example?Pablo
07/03/2025, 4:48 PMdata class RecipesScreenUiState(
val isLoading: Boolean = false,
val recipeMap: Map<String, String> = emptyMap(),
val recipeMapUpdateCount: Int = 0
)
class RecipesScreenViewModel(
private val dataStore: RecipeDataStore
) : ViewModel() {
private val _uiState = MutableStateFlow(RecipesScreenUiState(isLoading = true))
val uiState: StateFlow<RecipesScreenUiState> = _uiState
init {
viewModelScope.launch {
launch(<http://Dispatchers.IO|Dispatchers.IO>) {
dataStore.readRecipeMap().collect { recipes ->
_uiState.update { current ->
current.copy(
isLoading = false,
recipeMap = recipes,
recipeMapUpdateCount = current.recipeMapUpdateCount + 1
)
}
}
}
}
}
}
and the screen:
@Composable
fun RecipesScreen(
modifier: Modifier = Modifier,
vm: RecipesScreenViewModel = koinViewModel()
) {
val uiState by vm.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = modifier.fillMaxSize()
) {
RecipeListPane(
modifier = Modifier.fillMaxSize(),
recipes = uiState.recipeMap,
onItemClick = { recipeId ->
vm.fetchRecipeDetails(context, recipeId)
},
onDeleteClick = { recipeId ->
vm.selectRecipe(recipeId, null)
vm.showDeleteConfirmationDialog(true)
},
onEditClick = { recipeId, recipeName ->
vm.selectRecipe(recipeId, recipeName)
vm.toggleAddEditRecipeDialog()
}
)
}
}
}
Pablo
07/03/2025, 5:07 PMPablo
07/03/2025, 5:08 PMSeri
07/03/2025, 5:09 PMisLoading
it may be because only going from true to false causes a recomposition, while your subsequent overwrites (false->false) do notPablo
07/03/2025, 5:15 PM_uiState.update { it.copy(loading = true) }
_uiState.update { it.copy(map = newMap) }
_uiState.update { it.copy(loading = false) }
Pablo
07/03/2025, 5:15 PMSeri
07/03/2025, 5:55 PMPablo
07/04/2025, 8:05 AMPablo
07/04/2025, 8:05 AMPablo
07/04/2025, 8:06 AM