Tobias M.
Tobias M.

Reputation: 111

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

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.

My Model:

I am simulating a Storage system for ingredients, where

data class Ingredient(val name: String, @DrawableRes val iconResource: Int? = null)
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

    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:

What I would like to learn from you guys:

Upvotes: 11

Views: 10843

Answers (2)

Alejandra
Alejandra

Reputation: 882

  1. You can either remove the item and re-add it with the new value, this will cause recomposition to the list as the change is structural. The con is that depending on your list implementation this might not be O(1).

  2. You can use Compose State to hold and mutate this state. This will cause recomposition as youre writing to snapshot state.

These two options are discussed here when working with the checkbox: https://developer.android.com/codelabs/jetpack-compose-state#11

"This is because what Compose is tracking for the MutableList are changes related to adding and removing elements. This is why deleting works. But it's unaware of changes in the row item values (checkedState in our case), unless you tell it to track them too."

Upvotes: 5

Arpit Shukla
Arpit Shukla

Reputation: 10493

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)
    }
}

Upvotes: 4

Related Questions