Reputation: 31
I have a list of notifications in a ViewModel.
class Notification(
val id: String,
val title: String,
val body: String,
val time: String,
isRead: Boolean = false,
) {
var isRead by mutableStateOf(isRead)
}
class NotificationViewModel : ViewModel() {
private val _notificationList = mutableStateListOf(
Notification(
"ID1",
"Nice title",
"Nice description",
"18.03.2022 11:30"
),
Notification(
"ID2",
"Nice title",
"Nice description",
"18.03.2022 11:30"
),
Notification(
"ID3",
"Nice title",
"Nice description",
"18.03.2022 11:30"
),
)
val notificationList: List<Notification>
get() = _notificationList
fun deleteNotification(id: String) {
val list = _notificationList
val notification = list.find { id == it.id }
if (notification != null) {
list.remove(notification)
}
}
fun setRead(id: String, isRead: Boolean = true) {
val list = _notificationList
val notification = list.find { id == it.id }
if (notification != null) {
notification.isRead = isRead
}
}
fun setAllRead(isRead: Boolean = true) {
val list = _notificationList
if (list.isEmpty()) return
if (list.none { it.isRead != isRead }) return
list.forEachIndexed { _, n ->
n.isRead = isRead
}
}
}
And I use it in a notification screen where I can mark notifications as read and delete them.
@Composable
fun NotificationScreen(viewModel: NotificationViewModel) {
val list = viewModel.notificationList
Surface(modifier = Modifier.fillMaxSize()) {
LazyColumn {
item(key = "MarkAsRead") {
val setAllReadEnabled by remember(list) { derivedStateOf { list.find { !it.isRead } != null } }
Button(
enabled = setAllReadEnabled,
onClick = {
viewModel.setAllRead()
},
shape = MaterialTheme.shapes.extraLarge
) {
Text(
text = "Mark everything as read",
)
}
}
items(
items = list,
key = { it.id }
) { item ->
NotificationCard(
title = item.title,
body = item.body,
time = item.time,
isRead = item.isRead,
onDismiss = {
viewModel.deleteNotification(item.id)
},
onIconClick = {
viewModel.setRead(item.id, true)
}
)
}
}
}
}
The problem is that when I delete an element from the list all the items in LazyColumn get recomposed, even where the button is located. Reports tell me that Notification, NotificationViewModel and NotificationCardColors are stable. NotificationScreen and NotificationCard are skippable. I have no idea why it's happening or what I'm doing wrong.
I tried absolutely everything that I could find, but nothing worked. Also, I have almost same issue with my calendar component, where every day in the calendar grid gets recomposed when a day was selected.
Upd. Here's NotificationCard:
@Composable
fun NotificationCard(
modifier: Modifier = Modifier,
title: String,
body: String,
time: String,
isRead: Boolean = false,
colors: NotificationCardColors = DefaultNotificationCardColors.notificationCardColors(),
onDismiss: () -> Unit,
onIconClick: () -> Unit,
) {
val containerColor by colors.containerColor(isRead)
val textColor by colors.textColor(isRead)
val timeColor by colors.timeColor(isRead)
val swipeableState = rememberSwipeableState(0)
val width = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() }
val anchors = mapOf(-width / 2 to -1, 0f to 0, width / 2 to 1)
val currentValue = swipeableState.currentValue
LaunchedEffect(key1 = currentValue) {
when (currentValue) {
-1, 1 -> onDismiss()
}
}
Surface(
modifier = modifier
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.8f) },
orientation = Orientation.Horizontal,
)
.offset {
IntOffset(
x = swipeableState.offset.value.dp
.toPx()
.toInt(), y = 0
)
},
color = containerColor,
contentColor = textColor,
shape = MaterialTheme.shapes.large,
shadowElevation = 5.dp,
) {
Column(
modifier = Modifier
.padding(12.dp),
verticalArrangement = spacedBy(12.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = SpaceBetween,
verticalAlignment = CenterVertically
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(
text = time,
style = MaterialTheme.typography.titleSmall,
color = timeColor,
)
}
Row {
Text(
modifier = Modifier.weight(.9f),
text = body,
style = MaterialTheme.typography.labelMedium,
softWrap = true,
)
Box(
modifier = Modifier.weight(.1f),
contentAlignment = Center,
) {
androidx.compose.animation.AnimatedVisibility(
visible = !isRead,
enter = Transitions.expandFadeIn(clip = false),
exit = Transitions.shrinkFadeOut(clip = false)
) {
IconButton(modifier = Modifier.size(24.dp), onClick = onIconClick) {
Icon(id = R.drawable.ic_check, tint = timeColor)
}
}
}
}
}
}
}
Upvotes: 3
Views: 312
Reputation: 8729
I can't spot any obvious error in your code, but how are you determining that every row is being recomposed? Are you sure it's not just the internal lazylist composables that are recomposing and that your notification card is being skipped correctly?
In the Layout inspector, you are looking for NotificationCard
(in my screenshot I had a row called InterestsItem
) and seeing if that is skipped. The composables above it are internal to lazy list and recomposition there is expected when the list changes.
Upvotes: 2