androidandroid-jetpack-composeandroid-roomkotlin-stateflow

Composable isn't updating when I change input to the Room using StateFlow and collectAsStateWithLifecycle


My UI is not updating to show the new list of items when I change customers.

I have a Composable screen to display the customer and a list of the items for that customer. When I select a new customer, the customer id on the screen changes, but the list of items for that customer does not get updated. I always only see the first list of items that was displayed. How do I get the UI to update?

CartViewModel:

class CartViewModel @Inject constructor(
    val app: Application,
    private val repository: CartRepository,
    private val pendingCustomerRepository: PendingCustomerRepository,
    private val offlineOrderRepo: OfflineOrderDetailRepository,
    ) : AndroidViewModel(app) {

    val offlineItemsStateFlow = offlineOrderRepo.getOfflineItemsForCustomer(currentCustomerId)
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), emptyList())

    fun getOfflineItemsForCustomer() {
        offlineItemsStateFlow = offlineOrderRepo.getOfflineItemsForCustomer(currentCustomerId)
    }
}

CartView:

@Composable
fun SharedTransitionScope.CartView(
    navController: NavController,
    cartViewModel: CartViewModel,
    onBack: ()->Unit,
) {

    val offlineItems by cartViewModel.offlineItemsStateFlow.collectAsStateWithLifecycle()
    var isChangeRolePopupOpen by remember { mutableStateOf(false) }

    Column(
        Modifier
            // consumeWindowInsets keep imePadding from doubling up
            .consumeWindowInsets(it)
            .imePadding()
    ) {
        Text("Current offline customer is: ${cartViewModel.currentCustomerId}")
        if (true) {
            cartViewModel.updateCurrentCustomerId()
        }
        OfflineOrderContent(
            orderList = offlineItems,
            updateOfflineItemQuantity = { it ->
                cartViewModel.updateOfflineItemQuantity(it)
            },
            onDeleteItem = { it ->
                cartViewModel.deleteOfflineItemFor(it)
            }
        )
    }
}

Solution

  • You use the MutableState currentCustomerId in your view model and expect any changes to trigger a re-execution of the repos getOfflineItemsForCustomer. That doesn't work, though, because a changed State only triggers the re-execution of Compose functions, i.e. those that are annotated with @Composable. And no, the solution is not to make your repository functions composables as well; only UI-related functions should be composables.

    Instead, replace your MutableState with a MutableStateFlow like this:

    private val _currentCustomerId = MutableStateFlow(SettingsHelper.getCustomerId(app))
    val currentCustomerId: StateFlow<String> = _currentCustomerId.asStateFlow()
    
    fun updateCurrentCustomerId() {
        _currentCustomerId.value = SettingsHelper.getCustomerId(app)
    }
    

    (Whatever SettingsHelper is; it seems like this should be moved to a Data Store or at least a SavedStateHandle instead)

    A MutableStateFlow is similar to a MutableState in that they share the same semantics: They are both a container that holds a value, which can be observed for changes. Despite their similar name they are entirely unrelated, though: The former is an integral component of the Kotlin programming language, where the latter is only part of the Compose framework from Google.

    currentCustomerId is now a StateFlow that you need to collect in your composables the same as you already did with offlineItems.

    Wih this out of the way, you can now simply base the database flow on this new _currentCustomerId flow:

    val offlineItemsStateFlow: StateFlow<List<OfflineOrderDetail>> = _currentCustomerId
        .flatMapLatest(offlineOrderRepo::getOfflineItemsForCustomer)
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5.seconds),
            initialValue = emptyList(),
        )
    

    flatMapLatest transforms the flow containing the customer id into a new flow. That is done by calling the repository's getOfflineItemsForCustomer function. If you are not familiar with function references (the syntax with ::), that is just short for this:

    .flatMapLatest { offlineOrderRepo.getOfflineItemsForCustomer(it) }
    

    And now the resulting flow is converted to a StateFlow as you did before (note that I also replaced 5_000L by 5.seconds which is more clear; you might need to update your imports as well)

    That's it: Whenever you call updateCurrentCustomerId to change _currentCustomerId, the offlineItemsStateFlow is updated with a new database flow for the new customer id. In consequence, your UI will automatically update as well.