Jetpack Compose LazyList列表变更全项重绘问题求助
问题:LazyColumn删除元素时全项重绘
在Jetpack Compose中,通过ViewModel维护通知列表,使用LazyColumn实现展示、标记已读和删除功能。但删除列表元素时,LazyColumn内所有项(包括顶部的"全部标记已读"按钮)都会重绘。已确认Notification、NotificationViewModel、NotificationCardColors为稳定类型,且NotificationScreen和NotificationCard已优化支持跳过重绘,仍无法解决问题,同时日历组件也存在选中后全项重绘的类似情况。
相关代码
Notification类
class Notification( val id: String, val title: String, val body: String, val time: String, isRead: Boolean = false, ) { var isRead by mutableStateOf(isRead) }
NotificationViewModel类
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 } } }
NotificationScreen组件
@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) } ) } } } }
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) } } } } } } }
原因分析
- 回调lambda不稳定:在
NotificationScreen的itemslambda中,每次重组都会为每个NotificationCard创建新的onDismiss和onIconClick实例。函数类型属于不稳定类型,Compose会判定NotificationCard输入参数变化,触发全项重绘。 - 顶部按钮依赖范围过大:
setAllReadEnabled通过derivedStateOf依赖整个列表,列表元素变化时必然触发按钮重绘(此逻辑合理,但可优化依赖粒度)。 - 稳定类型显式标记缺失:
Notification类未添加@Stable注解,可能导致Compose无法正确识别其稳定性,误触发重组。
解决方案
1. 显式标记Notification为稳定类型
添加@Stable注解,确保Compose正确识别实例稳定性:
import androidx.compose.runtime.Stable @Stable class Notification( val id: String, val title: String, val body: String, val time: String, isRead: Boolean = false, ) { var isRead by mutableStateOf(isRead) }
2. 稳定NotificationCard的回调lambda
用remember(item.id)包装回调,确保同一item的回调引用不变:
@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 -> val onDismiss = remember(item.id) { { viewModel.deleteNotification(item.id) } } val onIconClick = remember(item.id) { { viewModel.setRead(item.id, true) } } NotificationCard( title = item.title, body = item.body, time = item.time, isRead = item.isRead, onDismiss = onDismiss, onIconClick = onIconClick ) } } } }
3. 优化顶部按钮依赖粒度(可选)
在ViewModel中单独维护未读状态,避免依赖整个列表:
class NotificationViewModel : ViewModel() { private val _notificationList = mutableStateListOf<Notification>(...) val notificationList: List<Notification> get() = _notificationList val hasUnreadNotifications by derivedStateOf { _notificationList.any { !it.isRead } } // 其他方法保持不变 }
在NotificationScreen中直接使用该状态:
@Composable fun NotificationScreen(viewModel: NotificationViewModel) { val list = viewModel.notificationList val setAllReadEnabled by viewModel.hasUnreadNotifications Surface(modifier = Modifier.fillMaxSize()) { LazyColumn { item(key = "MarkAsRead") { Button( enabled = setAllReadEnabled, onClick = { viewModel.setAllRead() }, shape = MaterialTheme.shapes.extraLarge ) { Text(text = "Mark everything as read") } } // items部分保持优化后的代码 } } }
内容的提问来源于stack exchange,提问作者MiracetteNytten
相关产品推荐
相关产品推荐

