I’m using Jetpack Compose with StateFlow
in a ViewModel
. I have a Map<String, List<Int>>
and I want to sort it by the size of each list, then display the sorted result in the UI.
Inside my ViewModel, I perform the sorting like this:
private val _mapState = MutableStateFlow<Map<String, List<Int>>>(
mapOf(
"a" to listOf(1, 2, 3),
"b" to listOf(1),
"c" to listOf(1, 2)
)
)
val mapState: StateFlow<Map<String, List<Int>>> = _mapState
fun sortMapByListSize() {
val sortedMap = _mapState.value.entries
.sortedBy { it.value.size }
.associateTo(LinkedHashMap()) { it.toPair() }
println("Sorted in ViewModel: $sortedMap") // ✅ This prints the sorted map
_mapState.value = sortedMap // ❌ UI does not update
}
Then in my Composable, I collect the StateFlow like this:
@Composable
fun MapScreen(viewModel: MyViewModel = viewModel()) {
val map by viewModel.mapState.collectAsState()
LaunchedEffect(map) {
println("Map inside LaunchedEffect: $map")
}
Column {
Button(onClick = { viewModel.sortMapByListSize() }) {
Text("Sort")
}
}
}
The problem
Even though println("Sorted in ViewModel") shows that the map is sorted correctly, the Compose UI does not update and LaunchedEffect(map) does not get re-triggered.
I used LinkedHashMap to preserve order.
I verified that the map is correctly sorted inside ViewModel.
I tried using .toMap()
, .associate
, .associateTo(LinkedHashMap())
, etc.
I want ensure that Compose detects the change in order of the map values.
Maps have no sort order, so it doesn't matter what you do with the LinkedHashMap: As long as the map contains the same key/value mapping, the two maps are considered equal. The StateFlow, however, only emits a new value when the new value is not equal to the previous value. Just using a different map implementation doesn't change the map's equality, so the StateFlow also doesn't change.
See also How to check equality of LinkedHashMaps in Java - also taking the insertion-order into account?
That said, if the order of the elements is relevant, a Map isn't an appropriate data structure. You need a List for that. You can use a List<Pair>
as you already have as an intermediate result in sortMapByListSize
, define your own data type instead of Pair
, or refactor your data structure in some other way. What works best depends on the actual data you need to display.
As a closing note, I rarely find a good use case for Maps in Compose: Maps are like dictionaries where you can retrieve a value for a given key. But there is no natural UI element that represents a dictionary. There are natural UI elements for lists, though (like a Column or LazyColumn in Compose). The "dictionary" aspect of a Map would most closely match a list in the UI where you can select an element and display its details. But "selected element" is a state and should be moved to the view model, so what the UI really needs from the view model in this case is a (sorted) List and the details of the selected element - but never a Map itself, because it's not the UI that will need to lookup the details in the map.