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?

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
rysv
  • 2,416
  • 7
  • 30
  • 48

3 Answers3

20

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")
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
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.

Tobias
  • 294
  • 3
  • 13
0

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.

MrShakila
  • 874
  • 1
  • 4
  • 19
Mendroid
  • 1
  • 2