kotlinandroid-jetpack-composekotlin-coroutineskotlin-flowandroid-jetpack-compose-list

Jetpack compose collectAsState() is not collecting a hot flow when the list is modified


When I use collectAsState(), the collect {} is triggered only when a new list is passed, not when it is modified and emitted.

View Model

@HiltViewModel
class MyViewModel @Inject constructor() : ViewModel() {
    val items = MutableSharedFlow<List<DataItem>>()
    private val _items = mutableListOf<DataItem>()

    suspend fun getItems() {
        _items.clear()

        viewModelScope.launch {
            repeat(5) {
                _items.add(DataItem(it.toString(), "Title $it"))
                items.emit(_items)
            }
        }

        viewModelScope.launch {
            delay(3000)
            val newItem = DataItem("999", "For testing!!!!")
            _items[2] = newItem
            items.emit(_items)
            Log.e("ViewModel", "Updated list")
        }
    }
}

data class DataItem(val id: String, val title: String)

Composable

@Composable
fun TestScreen(myViewModel: MyViewModel) {
    val myItems by myViewModel.items.collectAsState(listOf())

    LaunchedEffect(key1 = true) {
        myViewModel.getItems()
    }

    LazyColumn(
        modifier = Modifier.padding(vertical = 20.dp, horizontal = 10.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(myItems) { myItem ->
            Log.e("TestScreen", "Got $myItem") // <-- won't show updated list with "999"
        }
    }
}

I want the collect {} to receive the updated list but it is not. SharedFlow or StateFlow does not matter, both behave the same. The only way I can make it work is by creating a new list and emit that. When I use SharedFlow it should not matter whether equals() returns true or false.

    viewModelScope.launch {
        delay(3000)
        val newList = _items.toMutableList()
        newList[2] = DataItem("999", "For testing!!!!")
        items.emit(newList)
        Log.e("ViewModel", "Updated list")
    }

I should not have to create a new list. Any idea what I am doing wrong?


Solution

  • You emit the same object every time. Flow doesn't care about equality and emits it - you can try to collect it manually to check it, but Compose tries to reduce the number of recompositions as much as possible, so it checks to see if the state value has actually been changed.

    And since you're emitting a mutable list, the same object is stored in the mutable state value. It can't keep track of changes to that object, and when you emit it again, it compares and sees that the array object is the same, so no recomposition is needed. You can add a breakpoint at this line to see what's going on.

    The solution is to convert your mutable list to an immutable one: it's gonna be a new object each on each emit.

    items.emit(_items.toImmutableList())
    

    An other option to consider is using mutableStateListOf:

    private val _items = mutableStateListOf<DataItem>()
    val items: List<DataItem> = _items
    
    suspend fun getItems() {
        _items.clear()
    
        viewModelScope.launch {
            repeat(5) {
                _items.add(DataItem(it.toString(), "Title $it"))
            }
        }
    
        viewModelScope.launch {
            delay(3000)
            val newItem = DataItem("999", "For testing!!!!")
            _items[2] = newItem
            Log.e("ViewModel", "Updated list")
        }
    }