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

Time:03-27

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?

CodePudding user response:

This is the expected behavior of state and jetpack compose. Jetpack compose only recomposes if the value of the state changes. Since a list operation changes only the contents of the object, but not the object reference itself, the composition will not be recomposed.

CodePudding user response:

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")
    }
}
  • Related