Jetpack Compose: No recomposition happening, when updating list element contents

Viewed 2348

I am experimenting with Android's Jetpack Compose.
For simple use cases everything is working as expected,
but I am having some trouble with missing recompositions for a more advanced case.
This is my first post on stackoverflow. I hope I can describe my issue well enough to earn some help. ;)

My Model:

I am simulating a Storage system for ingredients, where

  • an ingredient consists of a name and an optional icon:
data class Ingredient(val name: String, @DrawableRes val iconResource: Int? = null)
  • a StorageItem consists of an ingredient and a stock (amount of this ingredient in storage):
data class StorageItem(val ingredient: Ingredient, var stock: Int)

My Composables:

My composables for the StorageUi are supposed to list all storage items
and display icon and name for the ingredient, as well as the stock.
For this post, I stripped it of all irrelevant modifiers and formatting to simplify readability.
(Note that I overloaded my StorageScreen composable with a second version without view model
for easier testing and in order to facilitate the Preview functionality in Android Studio.)

    @Composable
    fun StorageScreen(viewModel: StorageViewModel) {
        StorageScreen(
            navController = navController,
            storageItems = viewModel.storageItems,
            onIngredientPurchased = viewModel::purchaseIngredient
        )
    }

    @Composable
    fun StorageScreen(storageItems: List<StorageItem>, onIngredientPurchased: (StorageItem) -> Unit) {
        Column {
            TitleBar(...)
            IngredientsList(storageItems, onIngredientPurchased)
        }
    }

    @Composable
    private fun IngredientsList(storageItems: List<StorageItem>, onIngredientPurchased: (StorageItem) -> Unit) {
        LazyColumn {
            items(storageItems) { storageItem ->
                IngredientCard(storageItem, onIngredientPurchased)
            }
        }
    }

    @Composable
    private fun IngredientCard(storageItem: StorageItem, onIngredientPurchased: (StorageItem) -> Unit) {
        Card(
            Modifier.clickable { onIngredientPurchased(storageItem) }
        ) {
            Row {
                ImageIcon(...)

                Text(storageItem.ingredient.name)

                Text("${storageItem.stock}x")
            }
        }
    }

My View Model:

In my ViewModel, I

  • create a mutable state list (initialization with data not shown here)
  • provide the event handler that increases the stock, if the user taps an ingredient card
    class StorageViewModel : ViewModel() {

        var storageItems = mutableStateListOf<StorageItem>()
            private set

        fun purchaseIngredient(storageItem: StorageItem) {
            storageItem.stock += 1
        }

    }

The Problem: No recomposition takes place when changing the stock of an ingredient

I tried changing the event handler to simply remove the tapped item from the list:

        fun purchaseIngredient(storageItem: StorageItem) {
            storageItems.remove(storageItem)
        }

And voilĂ , the UI recomposes and the tapped ingredient is gone.

What I learned:

  • mutableStateListOf() does observe changes to the list (add, remove, reorder)
  • mutableStateListOf() does NOT observe changes to elements within the list (ingredient name/icon/stock changes)

What I would like to learn from you guys:

  • How would you go about solving this issue?
  • What can I do to achieve a recomposition, if any element within the list changes its state?
1 Answers

What can I do to achieve a recomposition, if any element within the list changes its state?

Recomposition will happen only when you change the list itself. You can do it this way.

class StorageViewModel : ViewModel() {

     var storageItems by mutableStateOf(emptyList<StorageItem>())
        private set

     fun purchaseIngredient(storageItem: StorageItem) {
        storageItems = storageItems.map { item ->
            if(item == storageItem)
                item.copy(stock = item.stock + 1)
            else
                item
        }
     }
}

Since this is a very common operation, you can create an extension function to make it look a little nicer.

fun <T> List<T>.updateElement(predicate: (T) -> Boolean, transform: (T) -> T): List<T> {
    return map { if (predicate(it)) transform(it) else it }
}

fun purchaseIngredient(storageItem: StorageItem) {
    storageItems = storageItems.updateElement({it == storageItem}) {
        it.copy(stock = it.stock + 1)
    }
}

Related