androidkotlincollectionsandroid-jetpack-composekotlin-stateflow

State flow doesn't emit new value when sorting a Map


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.


Solution

  • 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.