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

3

12

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?

Emphasis answered 27/3, 2022 at 2:51 Comment(0)
J
23

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")
    }
}
Jeer answered 27/3, 2022 at 5:19 Comment(0)
C
3

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.

Ceres answered 27/3, 2022 at 3:51 Comment(0)
M
1

Had the same problem with the Room database. Every new row in Database was updated in State too, but changes inside the Entity are not visible. Following Phil Dukhov's solution I've changed my viewModel so that every change in the database updates the UI:

class MemoViewModel(val noteDao: NoteDao):ViewModel() {
companion object {
    private const val TIMEOUT_MILLIS = 5_000L
}

val homeUiState: StateFlow<HomeUiState> =
    noteDao.getAllNotes().map { it: List<Note>
        HomeUiState(it.toMutableStateList())
    }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(TIMEOUT_MILLIS),
            initialValue = HomeUiState(mutableStateListOf())
        )
...
/**
 * Ui State for HomeScreen
 */
data class HomeUiState(
    val noteList: SnapshotStateList<Note>
)

The solution here is actually the Type: SnapshotStateList <> result of Function mutableStateListOf() that observes every change in the List. That solution is very nice because my DAO still returns List<> and the syntax on List<> and SnapshotStateList<> is the same. Composable and StateCollector need no changes too.

Melody answered 20/4, 2023 at 17:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.