I'm working on an Android project, with Kotlin and Jetpack Compose. I essentially have a list which the user can add items to or delete items from (details below). I would like to do something when an item is added to the list, such as scroll the list down if the item is out of sight. (But this same principle should apply in many other contexts/situations, not just lists.)
I could try using a side effect in the (compose) UI. But this would have to be triggered whenever the list changes, and in that case it would do the action after an item is deleted (or after items are reordered, if that were possible), since there's no straight-forward way to compare the new list to the old list (any attempt to do this feels like a work-around, rather than a proper solution).
The view model and repository (details below) have functions which add an item to the end of the list. So another potential solution is to send an 'event' (via state, or a channel) to the UI, telling it to scroll. The problem here is that the repository and view model are exposing the list as a flow, so it seems like there are potential race conditions where the 'event' to scroll the list might arrive at the UI before the new list flows to the UI, in which case it scrolls before the item gets added.
Basically, this UI action feels like it should be triggered either by the list flow emitting a new value - but then there's no way to know which user action made that happen - or by the repository/view model successfully adding the new item - but then 2 new values are racing up to the UI.
It seems like a common thing, so surely there's a solution out there.
Simplified code:
UI:
@Composable
fun MyScreen() {
val viewModel = viewModel<MyViewModel>()
val viewState by viewModel.viewState.collectAsStateWithLifecycle(ViewState())
// potentially react to changes in viewState here via side effect - but only after adding?
LazyColumn {
items(viewState.items, key = { it.id }) {
MyListItem(it)
}
}
}
View model:
class MyViewModel(private val repository: MyRepository) {
val viewState = repository.items.map { ViewState(items = it) }
fun addItem() {
viewModelScope.launch {
repository.addItem()
// potentially wait for the above to finish, then notify the UI here - but race conditions?
}
}
}
Repository: (Currently my repository (a singleton) just has a private mutable state flow holding the list. But I plan to have the repository save the list in a room database in the future.)
class MyRepositoryImpl: MyRepository {
private val _items = MutableStateFlow(emptyList())
override suspend fun addItem() {
val newItem = Item()
_items.update { it.toMutableList().apply { add(newItem) } }
}
}
Instead of exposing the item list in the flow, wrap the item list in another object that also contains a "last action" or "event" variable that describes the latest update to the list (item added, item removed, item changed, etc.). Then emit this object in the flow, whenever the item list is updated.
The flow listener can then use the "last action" variable to determine what (if any) additional behaviour should be done (scroll to new item, etc.).
In this way you won't have any race-conditions and you won't have to synchronize updates to multiple objects in order to coordinate the data update and the "event" notification.