Список моделей Jetpack Compose перемешивается при добавлении новых элементов

У меня проблема с тем, что Jetpack compose отображает модель, содержащую ModelList элементов. Когда добавляются новые элементы, порядок элементов пользовательского интерфейса становится неправильным.

Вот очень простой CounterModel, содержащий ModelList из ItemModels:

@Model
data class CounterModel(
    var counter: Int = 0,
    var name: String = "",
    val items: ModelList<ItemModel> = ModelList()
)

@Model
data class ItemModel(
    var name: String
)

На экране отображаются две строки карточек для каждого ItemModel: RowA и RowB. Когда я создаю этот экран, инициализируется следующим CounterModel:

val model = CounterModel()
model.name="hi"
model.items.add(ItemModel("Item 1"))
model.items.add(ItemModel("Item 2"))
CounterModelScreen(model)

... он отображается, как и ожидалось, вот так:

Позиция 1 Ряд A

Позиция 1 Ряд B

Позиция 2 Ряд A

Позиция 2 Ряд B

Когда я нажимаю кнопку "добавить" для вставки нового ItemModel, я просто ожидаю увидеть

Позиция 3 Ряд A

Позиция 3 Ряд B

Внизу. Но вместо этого порядок беспорядочный, и я вижу две rowAs, а затем две rowB:

Позиция 1 Ряд A

Позиция 1 Ряд B

Позиция 2 Ряд A

Позиция 3 Ряд A

Позиция 3 Ряд B

Позиция 2 Ряд B

Я не очень понимаю, как это возможно. Код пользовательского интерфейса чрезвычайно прост: переберите items и испустите RowA и RowB для каждого из них:

for (i in counterModel.items.indices) {
    RowA(counterModel, i)
    RowB(counterModel, i)
}

Использование Android Studio 4.0C6

Вот полный код:

@Composable
fun CounterModelScreen(counterModel: CounterModel) {
    Column {
        TopAppBar(title = {
            Text(
                text = "Counter Model"
            )
        })

        CounterHeader(counterModel)
        for (i in counterModel.items.indices) {
            RowA(counterModel, i)
            RowB(counterModel, i)
        }
        Button(
            text = "Add",
            onClick = {
                counterModel.items.add(ItemModel("Item " + (counterModel.items.size + 1)))
            })
    }
}

@Composable
fun CounterHeader(counterModel: CounterModel) {
    Text(text = counterModel.name)
}

@Composable
fun RowA(counterModel: CounterModel, index: Int) {
    Padding(padding = 8.dp) {
        Card(color = Color.White, shape = RoundedCornerShape(4.dp)) {
            Column(crossAxisSize = LayoutSize.Expand) {
                Text(
                    text = counterModel.items[index].name
                )
                Text(text = "Row A")
            }
        }
    }
}

@Composable
fun RowB(counterModel: CounterModel, index: Int) {
    Padding(padding = 8.dp) {
        Card(color = Color.Gray, shape = RoundedCornerShape(4.dp)) {
            Column(crossAxisSize = LayoutSize.Expand) {
                Text(
                    text = counterModel.items[index].name
                )
                Text(text = "Row B")
            }
        }
    }
}

person rockgecko    schedule 19.12.2019    source источник
comment
Я обнаружил, что удаление вызова CounterHeader(counterModel) непосредственно над циклом for решает проблему, но это меня еще больше сбивает с толку. CounterHeader Забава - это просто Text(text = counterModel.name), почему это должно на что-то влиять? Это даже работает, если я встраиваю Text (т.е. убираю веселье).   -  person rockgecko    schedule 20.12.2019
comment
Все еще встречается в Android Studio 4.0C7   -  person rockgecko    schedule 06.01.2020


Ответы (1)


Я протестировал его с помощью compose-1.0.0-alpha07 и внес некоторые изменения, чтобы адаптировать код к измененным API. Все работает безупречно, поэтому я предполагаю, что что-то было сломано в более старой версии compose, поскольку код выглядит правильно и работает в более поздних версиях с упомянутыми изменениями.

Я также изменил ваш код для использования состояний, как рекомендовано в документах, и добавил модель просмотра это поможет вам отделить представления от управления данными:

ViewModel

class CounterModelViewModel : ViewModel() {
    private val myBaseModel = CounterModel().apply {
        name = "hi"
        items.add(ItemModel("Item 1"))
        items.add(ItemModel("Item 2"))
    }

    private val _modelLiveData = MutableLiveData(myBaseModel)
    val modelLiveData: LiveData<CounterModel> = _modelLiveData

    fun addNewItem() {
        val oldCounterModel = modelLiveData.value ?: CounterModel()
        // Items is casted to a new MutableList because the new state won't be notified if the new
        // counter model content is the same one as the old one. You can also change any other
        // properties instead like the name or the counter
        val newItemsList = oldCounterModel.items.toMutableList()
        newItemsList.add(ItemModel("Item " + (newItemsList.size + 1)))
        
        // Pass a new instance of CounterModel to the LiveData
        val newCounterModel = oldCounterModel.copy(items = newItemsList)
        _modelLiveData.value = newCounterModel
    }
}

Составные представления обновлены:

@Composable
fun CounterModelScreen(counterModel: CounterModel, onAddNewItem: () -> Unit) {
    ScrollableColumn {
        TopAppBar(title = {
            Text(
                text = "Counter Model"
            )
        })

        CounterHeader(counterModel)
        counterModel.items.forEachIndexed { index, item ->
            RowA(counterModel, index)
            RowB(counterModel, index)
        }
        Button(
            onClick = onAddNewItem
        ) {
            Text(text = "Add")
        }
    }
}

@Composable
fun CounterHeader(counterModel: CounterModel) {
    Text(text = counterModel.name)
}

@Composable
fun RowA(counterModel: CounterModel, index: Int) {

    Card(
        backgroundColor = Color.White,
        shape = RoundedCornerShape(4.dp),
        modifier = Modifier.padding(8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth()) {
            Text(
                text = counterModel.items[index].name
            )
            Text(text = "Row A")
        }
    }

}

@Composable
fun RowB(counterModel: CounterModel, index: Int) {

    Card(
        backgroundColor = Color.Gray,
        shape = RoundedCornerShape(4.dp),
        modifier = Modifier.padding(8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth()) {
            Text(
                text = counterModel.items[index].name
            )
            Text(text = "Row B")
        }
    }

}

Этот предыдущий код вызывается из другой составной функции, которая содержит экземпляр ViewModel, однако вы можете изменить его на действие или фрагмент с экземпляром упомянутой ViewModel, это зависит от ваших предпочтений.

@Composable
fun MyCustomScreen(viewModel: CounterModelViewModel = viewModel()) {
    val modelState: CounterModel by viewModel.modelLiveData.observeAsState(CounterModel())

    CounterModelScreen(
        counterModel = modelState,
        onAddNewItem = {
            viewModel.addNewItem()
        }
    )
}
person Abdelilah El Aissaoui    schedule 30.11.2020