У меня есть класс данных
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?
Вы должны присвоить классу Item свойство Long id, присвоить каждому элементу уникальное значение и использовать его в качестве ключа.
Текущее время в миллисекундах также является хорошим уникальным идентификатором.
Кроме того, у вас неправильное понимание того, как работает компоновка. Дерево пометит все, от корня до листа, для перекомпоновки, а затем пропустит это в зависимости от стабильности. В вашем случае наличие var в классе данных делает класс нестабильным.
после замены var на val происходит перекомпозиция, но всего списка.
Предоставленный вами код должен работать по назначению (хотя я не совсем уверен в том, что вы используете 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
}
}
Это ошибка, склонная к выходу. Выбрано как
var. Вам не следует использовать хэш-код в качестве ключа, поскольку уникальность хэш-кодов не гарантируется. Ключевым моментом является то, как он узнает, представляют ли два разных экземпляра Item одну и ту же строку в списке. Поэтому, когда вы меняете isSelected, хеш-код для одного и того же элемента будет другим. Возможно, это источник вашей проблемы.