Having a uistate with a lot of variables, I have a...
# compose
p
Having a uistate with a lot of variables, I have a screen where I do this:
Copy code
val uiState by vm.uiState.collectAsStateWithLifecycle()
and inside that screen I have a composable which iterates one list of elements contained on that uistate:
Copy code
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.
s
When your
uiState.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.
Your
Map
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/stability
In order to make your Map snapshottable and stable for Recomposition, you should switch it over to use the
SnapshotStateMap
class, generated by
mutableStateMapOf(...)
https://developer.android.com/reference/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMap
p
but a uistate shouldn't have mutable variables
how can I deal with that?
p
on the other hand, seri, I tryed this:
Copy code
_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 ?
s
It's hard for me to tell without seeing the structure of your Composable function calls and how you're using
uiState
. Could you try reducing it into a smaller example?
p
Copy code
data 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:
Copy code
@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()
                }
            )
        }
    }
}
I never use recipeMapUpdateCount, but it helps recomposing if I increase it
on the other hand, modifying loading didn't help, can't see why
s
For
isLoading
it may be because only going from true to false causes a recomposition, while your subsequent overwrites (false->false) do not
p
Copy code
_uiState.update { it.copy(loading = true) }
_uiState.update { it.copy(map = newMap) }
_uiState.update { it.copy(loading = false) }
it's from true to false
s
In this example, you might be replacing the state faster than the Compose UI can collect changes, so the in-betweens get lost. It's a bit contrived, but maybe adding delays in there would give more understandable behavior?
p
well, finally I migrated the map to a list<pair<string,string>> and now everything works...
I'll try to remember that maps shouldn't be used in uistate 😞
it's a shame, because map is much faster than list for some kind of situations