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 kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
@ -34,6 +37,21 @@ data class CalendarDay(
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 周号计算逻辑
*
@ -118,6 +136,26 @@ class CalendarViewModel(
private val _showLegalHoliday = MutableStateFlow(false)
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)
) {
week.forEach { dayData ->
DayCell(
date = dayData.date,
isCurrentMonth = dayData.isCurrentMonth,
isSelected = dayData.date == selectedDate,
isToday = dayData.date == today,
shiftKind = shiftKindAt(dayData.date),
showLegalHoliday = showLegalHoliday,
onClick = { onDateClick(dayData.date) },
modifier = Modifier.weight(1f),
interactionSource = interactionSource
)
key(dayData.date) {
DayCell(
date = dayData.date,
isCurrentMonth = dayData.isCurrentMonth,
isSelected = dayData.date == selectedDate,
isToday = dayData.date == today,
shiftKind = shiftKindAt(dayData.date),
showLegalHoliday = showLegalHoliday,
onClick = { onDateClick(dayData.date) },
modifier = Modifier.weight(1f),
interactionSource = interactionSource
)
}
}
}
}

View File

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