From 7f9db1dc1d3156e8c560f81685ad2f03f8c8be8b Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 22 May 2026 11:09:05 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E8=81=9A=E5=90=88=20CalendarUiState=20?= =?UTF-8?q?=E5=87=8F=E5=B0=91=20Compose=20=E9=87=8D=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../plus/rua/project/CalendarViewModel.kt | 38 +++++++++++++ .../plus/rua/project/ui/CalendarMonthPage.kt | 24 ++++---- .../plus/rua/project/ui/CalendarMonthView.kt | 57 +++++++++++-------- 3 files changed, 84 insertions(+), 35 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt index c5f1bfb..b3754b9 100644 --- a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt @@ -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 = _showLegalHoliday.asStateFlow() + /** 聚合 UI 状态,减少 Compose 层分散订阅导致的重组。 */ + val uiState: StateFlow = 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) + ) + /** * 选中指定日期。 * diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index cedd2a9..2aa63f9 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -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 + ) + } } } } diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt index fafd8c0..9354161 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -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() 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 无替代 API,kotlinx-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,