MutableStateListOf не перестраивается при обновлении одного элемента

У меня есть класс данных

data class Item(
    val name: String,
    var isSelected: Boolean,
)

У меня есть список с элементами

val itemStateList = mutableStateListOf<Item>()

У меня есть следующий LazyColumn

LazyColumn( modifier = Modifier.fillMaxWidth(), state = listState ) {
    
    itemsIndexed(itemList, key = { _, item: Item -> item.hashCode() }) { index, item ->
        
        val backgroundColor =
          if (itemList[index].isSelected)
                MaterialTheme.colorScheme.secondary
            else
                Color.Transparent

        CustomCard(index, background)

     }
}

и следующий метод в viewModel, который срабатывает каждый раз, когда я нажимаю на элемент в списке

fun setSelected(index: Int) {
    itemStateList[index] = itemStateList[index].copy(isSelected = true)
}

Я знаю, что обновление только isSelected не вызовет рекомпозицию, но использование метода копирования, описанного выше, должно вызвать рекомпозицию, но это не так. Как я могу заставить его работать так, чтобы при нажатии на один элемент перекомпоновывался только этот элемент на основе значения itemList[index].isSelected?

Это ошибка, склонная к выходу. Выбрано как var. Вам не следует использовать хэш-код в качестве ключа, поскольку уникальность хэш-кодов не гарантируется. Ключевым моментом является то, как он узнает, представляют ли два разных экземпляра Item одну и ту же строку в списке. Поэтому, когда вы меняете isSelected, хеш-код для одного и того же элемента будет другим. Возможно, это источник вашей проблемы.

Tenfour04 02.06.2024 16:16

Вы должны присвоить классу Item свойство Long id, присвоить каждому элементу уникальное значение и использовать его в качестве ключа.

Tenfour04 02.06.2024 16:19

Текущее время в миллисекундах также является хорошим уникальным идентификатором.

gtxtreme 02.06.2024 16:35

Кроме того, у вас неправильное понимание того, как работает компоновка. Дерево пометит все, от корня до листа, для перекомпоновки, а затем пропустит это в зависимости от стабильности. В вашем случае наличие var в классе данных делает класс нестабильным.

gtxtreme 02.06.2024 16:37

после замены var на val происходит перекомпозиция, но всего списка.

AashishKSahu 02.06.2024 19:53
1
5
61
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Предоставленный вами код должен работать по назначению (хотя я не совсем уверен в том, что вы используете LazyColumn key; см. ниже). Я предполагаю, что проблема где-то в вашем компонуемом элементе над LazyColumn. Но вместо того, чтобы пытаться это исправить, вам следует использовать совершенно другой подход. В общем, не следует использовать объекты State в модели представления, это может привести к некоторым сложным проблемам, особенно с модульными тестами. Вместо этого вам следует использовать MutableStateFlow:

private val _itemList = MutableStateFlow<List<Item>>(emptyList())
val itemList = _itemList.asStateFlow()

fun setSelected(indexToSelect: Int) = _itemList.update {
        it.mapIndexed { index, item ->
            if (index == indexToSelect) item.copy(isSelected = true)
            else item
        }
    }
}

В ваших составных объектах вы можете получить к ним доступ следующим образом:

val itemList by viewModel.itemList.collectAsStateWithLifecycle()

(Для этого вам понадобится зависимость Gradle androidx.lifecycle:lifecycle-runtime-compose)

Использование потока также облегчит задачу, если вы позже решите изменить источник списка на что-то другое, например на базу данных.

Теперь, хотя это успешно заменяет ваше состояние на поток, есть еще некоторые проблемы, которые необходимо решить. Во-первых, как уже упоминалось Tenfour04 в комментариях, ваш класс данных должен быть неизменяемым и var следует заменить на val.

Более того, LazyColumn кажется излишне сложным. Вам следует изменить его на это:

LazyColumn(modifier = Modifier.fillMaxWidth(), state = listState) {
    items(itemList) { item ->
        val backgroundColor = if (item.isSelected)
            MaterialTheme.colorScheme.secondary
        else
            Color.Transparent

        CustomCard(
            item = item,
            background = backgroundColor,
        ) {
            viewModel.setSelected(item.id)
        }
    }
}

Вам не нужен индекс и вам не нужен ключ. Однако самая важная часть — это изменение параметров CustomCard. Теперь весь элемент передан; в конце концов, эта карточка, вероятно, должна отображать name, поэтому сам по себе индекс бесполезен. Я также добавил последний параметр — функцию, которая должна выполняться, когда пользователь нажимает на карту. Вы не поставили CustomCard, но сейчас это могло бы выглядеть примерно так:

@Composable
fun CustomCard(
    item: Item,
    background: Color,
    modifier: Modifier = Modifier,
    onClick: () -> Unit,
) {
    ElevatedCard(
        onClick = onClick,
        modifier = modifier.fillMaxWidth(),
        colors = CardDefaults.elevatedCardColors().copy(
            containerColor = background,
        ),
    ) {
        Text(item.name)
    }
}

CustomCard теперь не нужно знать о модели представления и функции setSelected, все это скрыто параметром onClick.

В LazyColumn мы устанавливаем следующий параметр:

{ viewModel.setSelected(item.id) }

Возможно, вы уже поняли, что это не скомпилируется, потому что для id нет свойства Item. Однако это было сделано намеренно, поскольку нецелесообразно использовать индекс списка, который может измениться в любое время, для идентификации элемента. Если вы не можете гарантировать, что name уникален, вам понадобится новое свойство, которое может однозначно идентифицировать элемент:

data class Item(
    val id: Int,
    val name: String,
    val isSelected: Boolean,
)

Наконец, вам нужно обновить функцию setSelected модели представления, чтобы идентифицировать элемент по его идентификатору:

fun setSelected(id: Int) = _itemList.update {
    it.map { item ->
        if (item.id == id) item.copy(isSelected = true)
        else item
    }
}

Другие вопросы по теме