I have a data class
data class Item(
val name: String,
var isSelected: Boolean,
)
I have a list with items
val itemStateList = mutableStateListOf<Item>()
I have the following 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)
}
}
and the following method in the viewModel that triggers everytime I tap on an item in the list
fun setSelected(index: Int) {
itemStateList[index] = itemStateList[index].copy(isSelected = true)
}
I know that updating isSelected alone will not trigger a recomposition, but using the copy method above should trigger the recompostion, but it is not. How can I make it work so that when a single item is tapped, only that item is recomposed based on the value of itemList[index].isSelected
?
The code you provided should work as intended (I'm actually not absoultely sure about your usage of the LazyColumn's key
, though; see below). I guess the problem is somewhere in your composable above the LazyColumn. But instead of trying to fix that you should use another approach entirely. In general, you shouldn't use State objects in the view model, it can lead to some tricky problems, especially with unit tests. You should use a MutableStateFlow instead:
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
}
}
}
In you composables you can the access it like this:
val itemList by viewModel.itemList.collectAsStateWithLifecycle()
(You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose
for that)
Using a Flow will aslo make it easier if you later decide to change the source of the list to something else, like a database for example.
Now, although this successfully replaces your State with a Flow, there are some other things that need to be addressed. First off, as Tenfour04 already mentioned in the comments, your data class should be immutable and var
should be replaced with val
.
Furthermore the LazyColumn seems unnecessariliy complicated. You should change it to this:
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)
}
}
}
You don't need the index and you don't need the key. The most important part, however, is the change to the parameters of CustomCard
. The entire Item is passed now; after all that card should probably display the name
, so the index alone is not of much use. I also added a last parameter that is a function that should be executed when the user clicks on the Card. You didn't provide CustomCard
, but now it could look something like this:
@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
now doesn't need to know about the view model and the setSelected
function, that is all hidden by the onClick
parameter.
In the LazyColumn we set the paramter to this:
{ viewModel.setSelected(item.id) }
You might have already realized that this won't compile because there is no property id
for an Item
. That was on purpose, though, because it is not appropriate to use the index of a list that may change anytime to identify an item. Unless you can guarantee that name
is unique you will need a new property that can uniquely identify an Item:
data class Item(
val id: Int,
val name: String,
val isSelected: Boolean,
)
Finally you need to update the view model's setSelected
function to identify the Item by its id:
fun setSelected(id: Int) = _itemList.update {
it.map { item ->
if (item.id == id) item.copy(isSelected = true)
else item
}
}