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:
parent
baedf878b4
commit
7f9db1dc1d
@ -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)
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选中指定日期。
|
* 选中指定日期。
|
||||||
*
|
*
|
||||||
|
|||||||
@ -189,6 +189,7 @@ private fun WeekRow(
|
|||||||
.padding(vertical = ROW_PADDING_DP.dp)
|
.padding(vertical = ROW_PADDING_DP.dp)
|
||||||
) {
|
) {
|
||||||
week.forEach { dayData ->
|
week.forEach { dayData ->
|
||||||
|
key(dayData.date) {
|
||||||
DayCell(
|
DayCell(
|
||||||
date = dayData.date,
|
date = dayData.date,
|
||||||
isCurrentMonth = dayData.isCurrentMonth,
|
isCurrentMonth = dayData.isCurrentMonth,
|
||||||
@ -204,6 +205,7 @@ private fun WeekRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private data class DayData(
|
private data class DayData(
|
||||||
val date: LocalDate,
|
val date: LocalDate,
|
||||||
|
|||||||
@ -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 无替代 API,kotlinx-datetime 尚未提供新接口
|
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user