perf: 聚合 CalendarUiState 减少 Compose 重组

- ViewModel 新增 CalendarUiState 数据类,通过 combine + stateIn
  将 6 个独立 StateFlow 合并为单一 uiState 流
- CalendarMonthView 从 5 个分散 collectAsState 改为单个
  uiState.collectAsState,减少重组次数
- CalendarPagerArea 改为显式传参,解耦对 ViewModel 的直接引用
- CalendarMonthPage 中 DayCell 外层包裹 key(dayData.date),
  稳定跨帧重组

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-22 11:09:05 +08:00
parent baedf878b4
commit 7f9db1dc1d
3 changed files with 84 additions and 35 deletions

View File

@ -4,8 +4,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.datetime.DatePeriod import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
@ -34,6 +37,21 @@ data class CalendarDay(
val isSelected: Boolean val isSelected: Boolean
) )
/**
* 日历 UI 状态聚合用于减少 Compose 重组次数
*
* 将多个独立的 StateFlow 合并为单一状态流
* 避免 `collectAsState()` 分散订阅导致的重复重组
*/
data class CalendarUiState(
val selectedDate: LocalDate,
val isCollapsed: Boolean,
val isYearView: Boolean,
val yearViewYear: Int,
val collapseProgress: Float,
val showLegalHoliday: Boolean
)
/** /**
* 日历状态管理持有选中日期折叠状态和 ISO 周号计算逻辑 * 日历状态管理持有选中日期折叠状态和 ISO 周号计算逻辑
* *
@ -118,6 +136,26 @@ class CalendarViewModel(
private val _showLegalHoliday = MutableStateFlow(false) private val _showLegalHoliday = MutableStateFlow(false)
val showLegalHoliday: StateFlow<Boolean> = _showLegalHoliday.asStateFlow() val showLegalHoliday: StateFlow<Boolean> = _showLegalHoliday.asStateFlow()
/** 聚合 UI 状态,减少 Compose 层分散订阅导致的重组。 */
val uiState: StateFlow<CalendarUiState> = combine(
combine(_selectedDate, _isCollapsed, ::Pair),
combine(_isYearView, _yearViewYear, ::Pair),
combine(_collapseProgress, _showLegalHoliday, ::Pair)
) { dateCollapsed, yearViewYearPair, progressHoliday ->
CalendarUiState(
selectedDate = dateCollapsed.first,
isCollapsed = dateCollapsed.second,
isYearView = yearViewYearPair.first,
yearViewYear = yearViewYearPair.second,
collapseProgress = progressHoliday.first,
showLegalHoliday = progressHoliday.second
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
CalendarUiState(today, false, false, today.year, 0f, false)
)
/** /**
* 选中指定日期 * 选中指定日期
* *

View File

@ -189,17 +189,19 @@ private fun WeekRow(
.padding(vertical = ROW_PADDING_DP.dp) .padding(vertical = ROW_PADDING_DP.dp)
) { ) {
week.forEach { dayData -> week.forEach { dayData ->
DayCell( key(dayData.date) {
date = dayData.date, DayCell(
isCurrentMonth = dayData.isCurrentMonth, date = dayData.date,
isSelected = dayData.date == selectedDate, isCurrentMonth = dayData.isCurrentMonth,
isToday = dayData.date == today, isSelected = dayData.date == selectedDate,
shiftKind = shiftKindAt(dayData.date), isToday = dayData.date == today,
showLegalHoliday = showLegalHoliday, shiftKind = shiftKindAt(dayData.date),
onClick = { onDateClick(dayData.date) }, showLegalHoliday = showLegalHoliday,
modifier = Modifier.weight(1f), onClick = { onDateClick(dayData.date) },
interactionSource = interactionSource modifier = Modifier.weight(1f),
) interactionSource = interactionSource
)
}
} }
} }
} }

View File

@ -72,6 +72,7 @@ import kotlinx.datetime.number
import kotlinx.datetime.plus import kotlinx.datetime.plus
import kotlinx.datetime.todayIn import kotlinx.datetime.todayIn
import plus.rua.project.CalendarViewModel import plus.rua.project.CalendarViewModel
import plus.rua.project.ShiftKind
import plus.rua.project.composeTraceBeginSection import plus.rua.project.composeTraceBeginSection
import plus.rua.project.composeTraceEndSection import plus.rua.project.composeTraceEndSection
import kotlin.math.abs import kotlin.math.abs
@ -95,15 +96,16 @@ fun CalendarMonthView(
val viewModel = viewModel<CalendarViewModel>() val viewModel = viewModel<CalendarViewModel>()
val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) } val today = remember { Clock.System.todayIn(TimeZone.currentSystemDefault()) }
val selectedDate by viewModel.selectedDate.collectAsState() val uiState by viewModel.uiState.collectAsState()
val selectedDate = uiState.selectedDate
val currentYear = selectedDate.year val currentYear = selectedDate.year
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口 @Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val currentMonth = selectedDate.month.number val currentMonth = selectedDate.month.number
val isCollapsed by viewModel.isCollapsed.collectAsState() val isCollapsed = uiState.isCollapsed
val isYearView by viewModel.isYearView.collectAsState() val isYearView = uiState.isYearView
val yearViewYear by viewModel.yearViewYear.collectAsState() val yearViewYear = uiState.yearViewYear
val collapseProgress by viewModel.collapseProgress.collectAsState() val collapseProgress = uiState.collapseProgress
val showLegalHoliday by viewModel.showLegalHoliday.collectAsState() val showLegalHoliday = uiState.showLegalHoliday
val density = LocalDensity.current val density = LocalDensity.current
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@ -210,10 +212,21 @@ fun CalendarMonthView(
) )
with(sharedScope) { with(sharedScope) {
CalendarPagerArea( CalendarPagerArea(
viewModel = viewModel, selectedDate = selectedDate,
today = today, today = today,
isCollapsed = isCollapsed,
collapseProgress = collapseProgress,
showLegalHoliday = showLegalHoliday,
rowHeightPx = rowHeightPx, rowHeightPx = rowHeightPx,
screenWidthPx = screenWidthPx, screenWidthPx = screenWidthPx,
onDateClick = { date -> viewModel.selectDate(date) },
onMonthChanged = { year, month ->
@Suppress("DEPRECATION")
val date = if (year == today.year && today.month.number == month) today
else LocalDate(year, month, 1)
viewModel.selectDate(date)
},
shiftKindAt = { date -> viewModel.shiftKindAt(date) },
onRowHeightMeasured = { h -> onRowHeightMeasured = { h ->
if (h > 0) rowHeightPx = h if (h > 0) rowHeightPx = h
}, },
@ -408,19 +421,21 @@ private fun MenuIcon(color: Color, modifier: Modifier = Modifier) {
@Composable @Composable
private fun CalendarPagerArea( private fun CalendarPagerArea(
viewModel: CalendarViewModel, selectedDate: LocalDate,
today: LocalDate, today: LocalDate,
isCollapsed: Boolean,
collapseProgress: Float,
showLegalHoliday: Boolean,
rowHeightPx: Int, rowHeightPx: Int,
screenWidthPx: Int, screenWidthPx: Int,
onDateClick: (LocalDate) -> Unit,
onMonthChanged: (year: Int, month: Int) -> Unit,
shiftKindAt: (LocalDate) -> ShiftKind?,
onRowHeightMeasured: ((Int) -> Unit)?, onRowHeightMeasured: ((Int) -> Unit)?,
pagerState: PagerState, pagerState: PagerState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val collapseProgress by viewModel.collapseProgress.collectAsState()
val selectedDate by viewModel.selectedDate.collectAsState()
val isCollapsed by viewModel.isCollapsed.collectAsState()
val showLegalHoliday by viewModel.showLegalHoliday.collectAsState()
val interpolatedWeeks by remember { val interpolatedWeeks by remember {
derivedStateOf { derivedStateOf {
@ -469,7 +484,7 @@ private fun CalendarPagerArea(
WeekPager( WeekPager(
selectedDate = selectedDate, selectedDate = selectedDate,
today = today, today = today,
onDateClick = { date -> viewModel.selectDate(date) }, onDateClick = onDateClick,
onWeekChanged = { weekMonday -> onWeekChanged = { weekMonday ->
val weekSunday = weekMonday.plus(DatePeriod(days = 6)) val weekSunday = weekMonday.plus(DatePeriod(days = 6))
val date = when { val date = when {
@ -485,9 +500,9 @@ private fun CalendarPagerArea(
else -> weekMonday else -> weekMonday
} }
viewModel.selectDate(date) onDateClick(date)
}, },
shiftKindAt = { date -> viewModel.shiftKindAt(date) }, shiftKindAt = shiftKindAt,
showLegalHoliday = showLegalHoliday, showLegalHoliday = showLegalHoliday,
modifier = pagerModifier modifier = pagerModifier
) )
@ -495,18 +510,12 @@ private fun CalendarPagerArea(
CalendarPager( CalendarPager(
selectedDate = selectedDate, selectedDate = selectedDate,
today = today, today = today,
onDateClick = { date -> viewModel.selectDate(date) }, onDateClick = onDateClick,
onMonthChanged = { year, month -> onMonthChanged = onMonthChanged,
@Suppress("DEPRECATION") // monthNumber 无替代 API
val date =
if (year == today.year && today.month.number == month) today
else LocalDate(year, month, 1)
viewModel.selectDate(date)
},
collapseProgress = collapseProgress, collapseProgress = collapseProgress,
rowHeightPx = rowHeightPx, rowHeightPx = rowHeightPx,
effectiveWeeks = effectiveWeeks, effectiveWeeks = effectiveWeeks,
shiftKindAt = { date -> viewModel.shiftKindAt(date) }, shiftKindAt = shiftKindAt,
showLegalHoliday = showLegalHoliday, showLegalHoliday = showLegalHoliday,
onRowHeightMeasured = onRowHeightMeasured, onRowHeightMeasured = onRowHeightMeasured,
pagerState = pagerState, pagerState = pagerState,