Landry Norris
07/28/2021, 8:46 PMLandry Norris
07/28/2021, 8:46 PM@Composable
fun TestCompose() {
val oldList = remember {
arrayListOf(1,2,3)
}
val listState = remember { mutableStateOf(oldList)}
Column {
Text(text = listState.value.size.toString())
Button(onClick = { oldList[1] = 3; listState.value = arrayListOf(1,3,3) }) {
Text("Modify")
}
LazyColumn {
itemsIndexed(listState.value) { _, value ->
Text("value: $value")
}
}
}
}
Colton Idle
07/28/2021, 8:47 PMLandry Norris
07/28/2021, 8:48 PMLandry Norris
07/28/2021, 8:49 PMCasey Brooks
07/28/2021, 8:59 PMmutableStateOf
has a configurable SnapshotMutationPolicy
which defaults to structuralEqualityPolicy
(checking whether the two lists are ==
). Since you change oldList
before listState
, it will see the new value as equal to the old value, and the UI will not update.
Button(onClick = {
oldList[1] = 3 // oldList is now [1,3,3]. listState is backed by that variable, so internally it is also [1,3,3], but the change could not be detected
listState.value = arrayListOf(1,3,3) // when you try to update this, the backing list is already [1,3,3], so the update is ignored
}) { ... }
You can use a referentialEqualityPolicy
instead which should treat it as you expect (recomposing when !==
)Landry Norris
07/28/2021, 9:01 PMLandry Norris
07/28/2021, 9:02 PMCasey Brooks
07/28/2021, 9:05 PMEvery time there would be new value posted into the Flow the returned State will be updatedI believe
collectAsState
is ulimately just posting to a mutableStateOf
internally, so it is likey using referential equality. The flow itself would be responsible for conflating (or not) those updates. Note that StateFlow
does conflate updates with structural equality, but there it would be the Flow, not Compose, preventing the updatesCasey Brooks
07/28/2021, 9:09 PMmutableStateOf
, mutableStateListOf
, etc. to ensure that nothing even could get changed without Compose knowing about it. Assuming you’re using the data correctly, Compose should just work, and you shouldn’t have to think any more about itLandry Norris
07/28/2021, 9:09 PMLandry Norris
07/28/2021, 9:15 PMCasey Brooks
07/28/2021, 9:15 PMFlow
, and the libraries are internally tracking all changes. So just observe your query as a flow and make updates directly to the database from the VM, the query should emit the new results afterward automaticallyLandry Norris
07/28/2021, 9:17 PMCasey Brooks
07/28/2021, 9:20 PMLandry Norris
07/28/2021, 9:22 PMCasey Brooks
07/28/2021, 9:27 PMCasey Brooks
07/28/2021, 9:45 PMclass MyViewModel(
val db: AppDatabase
) {
class State(
private val originalList: List<Record> = emptyList(),
val searchTerm: String = "",
) {
val displayedList: List<Record> = originalList
.filter { it.matchesSearchTerm(searchTerm) }
}
private val _state = MutableStateFlow(State())
val state: Flow<State> get() = _state
suspend fun initialize() {
db.observeRecords().collect { updatedResults ->
_state.value = _state.value.copy(
originalList = updatedResults
)
}
}
suspend fun searchTermUpdated(searchTerm: String) {
// this change will cause `state` to re-emit with the current
// list, but filtered by the new search term
_state.value = _state.value.copy(
searchTerm = searchTerm
)
}
suspend fun deleteRecord(record: Record) {
// this change will cause `observeRecords` to re-emit, which will
// then get filtered by the current search term
db.delete(record.id)
}
}
Landry Norris
07/28/2021, 9:52 PMCasey Brooks
07/28/2021, 10:00 PMState
object and zip
-ing the DB flow with that State flow.
class MyViewModel(
val db: AppDatabase
) {
class State(
val searchTerm: String = "",
)
private val _state = MutableStateFlow(State())
fun initialize(): Flow<List<Record>> {
return db.observeRecords()
.zip(_state) { records, state ->
records.filter { it.matchesSearchTerm(state.searchTerm) }
}
}
suspend fun searchTermUpdated(searchTerm: String) {
// this change will cause `state` to re-emit with the current
// list, but filtered by the new search term
_state.value = _state.value.copy(
searchTerm = searchTerm
)
}
suspend fun deleteRecord(record: Record) {
// this change will cause `observeRecords` to re-emit, which will
// then get filtered by the current search term
db.delete(record.id)
}
}
Personally, I would’t recommend the second approach as the first is more easily generalizable to a variety of use-cases. Once you need to observe multiple reactive sources (multiple DB queries, or listening to changes from a websocket, using a hierarchy of VMs where the parent State flows into the child, etc), it becomes harder to manage all that. But if all those reactive sources ultimately just dump their stuff into a single State
class which is all the UI sees, it’s easy to reason about any given screen in the same way and adapt it to those different situationseygraber
07/28/2021, 10:01 PMLandry Norris
07/28/2021, 10:01 PMCasey Brooks
07/28/2021, 10:25 PMzip
combines pairs of elements from two flows at the same index. .combine
is actually what it should be https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/combine.html