简体   繁体   English

Jetpack Compose:更新列表元素内容时不会发生重组

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

I am experimenting with Android's Jetpack Compose.我正在试验 Android 的 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.这是我在 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):一个StorageItem由一个成分和一个库存组成(该成分在存储中的数量):
data class StorageItem(val ingredient: Ingredient, var stock: Int)

My Composables :我的组合物

My composables for the StorageUi are supposed to list all storage items我的StorageUi组合应该列出所有存储项目
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 (请注意,我使用没有视图模型的第二个版本重载了我的 StorageScreen 可组合
for easier testing and in order to facilitate the Preview functionality in Android Studio.)以便于测试并促进 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在我的 ViewModel 中,我

  • 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() 确实观察列表的变化(添加、删除、重新排序)
  • mutableStateListOf() does NOT observe changes to elements within the list (ingredient name/icon/stock changes) mutableStateListOf() 不观察列表中元素的变化(成分名称/图标/库存变化)

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?如果列表中的任何元素更改其状态,我该怎么做才能实现重组?

Use a MutableStateFlow in the ViewModel, and then use collectAsState() in the Composable to update the UI when the data changes.在 ViewModel 中使用collectAsState() ,然后在 Composable 中使用collectAsState()在数据发生变化时更新 UI。

From the collectAsState() source:collectAsState()来源:

Collects values from this StateFlow and represents its latest value via State.从此 StateFlow 收集值并通过 State 表示其最新值。 The StateFlow.value is used as an initial value. StateFlow.value 用作初始值。 Every time there would be new value posted into the StateFlow the returned State will be updated causing recomposition of every State.value usage.每次将新值发布到 StateFlow 时,返回的 State 将被更新,从而导致每个 State.value 用法的重新组合。

So in your ViewModel you would change it to use a MutableStateFlow (initializing to an empty list for simplicity):因此,在您的 ViewModel 中,您可以将其更改为使用 MutableStateFlow(为简单起见,初始化为空列表):

class StorageViewModel : ViewModel() {

    var storageItems = MutableStateFlow<List<StorageItem>>(emptyList())
        private set

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

Then in the Composable you use the collectAsState() and just feed in the current value:然后在 Composable 中使用collectAsState()并输入当前值:

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM