From 774e03a928402cccece81ec52cdb636075d902b2 Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 21 May 2026 17:58:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20Wave=202=20=E2=80=94=20LunarCache?= =?UTF-8?q?=20=E5=8F=AF=E6=B3=A8=E5=85=A5=E5=8C=96=20+=20=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E8=AE=A1=E7=AE=97=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - refactor: LunarCache 从 object 单例改为 class + Mutex,方法改为 suspend - refactor: DayCell 参数注入 lunarCache,remember → produceState - refactor: 提取 MonthGridInfo/getMonthGridInfo 到 CalendarUtils - refactor: CalendarViewModel init 块和 getMonthDays 复用 getMonthGridInfo - refactor: calculateWeeksCount 委托给 getMonthGridInfo Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plus/rua/project/CalendarViewModel.kt | 34 +++++---------- .../kotlin/plus/rua/project/LunarCache.kt | 43 +++++++++++-------- .../kotlin/plus/rua/project/ui/BottomCard.kt | 9 +++- .../plus/rua/project/ui/CalendarUtils.kt | 38 +++++++++++++--- .../kotlin/plus/rua/project/ui/DayCell.kt | 16 +++++-- 5 files changed, 89 insertions(+), 51 deletions(-) diff --git a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt index ba729ca..f8b9a76 100644 --- a/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt @@ -23,6 +23,7 @@ import kotlinx.datetime.todayIn import plus.rua.project.LunarCache import plus.rua.project.ui.COLLAPSE_THRESHOLD import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP +import plus.rua.project.ui.getMonthGridInfo import kotlin.time.Clock /** @@ -64,26 +65,19 @@ class CalendarViewModel( currentMonth to currentYear, currentMonth + 1 to currentYear ).map { (month, year) -> - when { + val (normalizedMonth, normalizedYear) = when { month < 1 -> 12 to year - 1 month > 12 -> 1 to year + 1 else -> month to year } + getMonthGridInfo(normalizedYear, normalizedMonth) } - monthsToPrecompute.forEach { (month, year) -> - val firstOfMonth = LocalDate(year, month, 1) - val offset = firstOfMonth.dayOfWeek.ordinal - val startDate = firstOfMonth.minus(DatePeriod(days = offset)) - val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1) - val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).day - val rows = ((offset + daysInMonth - 1) / 7) + 1 - val totalDays = rows * 7 - - val dates = (0 until totalDays).map { i -> - startDate.plus(DatePeriod(days = i)) + monthsToPrecompute.forEach { info -> + val dates = (0 until info.totalDays).map { i -> + info.startDate.plus(DatePeriod(days = i)) } - LunarCache.precompute(dates) + LunarCache.default.precompute(dates) } } } @@ -327,17 +321,9 @@ class CalendarViewModel( @Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口 fun getMonthDays(year: Int, month: Int): List { composeTraceBeginSection("getMonthDays:$year-$month") - val firstOfMonth = LocalDate(year, month, 1) - val dayOfWeekOffset = firstOfMonth.dayOfWeek.ordinal - val startDate = firstOfMonth.minus(DatePeriod(days = dayOfWeekOffset)) - val nextMonth = - if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1) - val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).day - val rows = ((dayOfWeekOffset + daysInMonth - 1) / 7) + 1 - val totalDays = rows * 7 - - val result = (0 until totalDays).map { i -> - val date = startDate.plus(DatePeriod(days = i)) + val info = getMonthGridInfo(year, month) + val result = (0 until info.totalDays).map { i -> + val date = info.startDate.plus(DatePeriod(days = i)) CalendarDay( date = date, isCurrentMonth = date.month.number == month && date.year == year, diff --git a/core/src/main/kotlin/plus/rua/project/LunarCache.kt b/core/src/main/kotlin/plus/rua/project/LunarCache.kt index 8e33d9a..3fa733e 100644 --- a/core/src/main/kotlin/plus/rua/project/LunarCache.kt +++ b/core/src/main/kotlin/plus/rua/project/LunarCache.kt @@ -1,31 +1,36 @@ package plus.rua.project import com.tyme.solar.SolarDay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.datetime.LocalDate /** * 农历/节气/节假日信息缓存。 * - * 使用 LinkedHashMap(accessOrder=true)实现 LRU 语义,读写速度优于 ConcurrentHashMap。 - * 通过 @Synchronized 保护并发访问;冷启动时主线程单线程访问,偏向锁使其几乎零开销。 + * 使用 LinkedHashMap(accessOrder=true)实现 LRU 语义。 + * 通过 [Mutex] 保护并发访问,协程友好,不阻塞线程。 + * + * @param maxSize 缓存最大容量,默认 800 */ -object LunarCache { - private const val MAX_SIZE = 800 +class LunarCache( + private val maxSize: Int = MAX_SIZE +) { + private val mutex = Mutex() @Suppress("DEPRECATION") private val cache = LinkedHashMap(256, 0.75f, true) /** - * 获取指定日期的信息,缓存 miss 时同步计算。 + * 获取指定日期的信息,缓存 miss 时计算。 */ - @Synchronized @Suppress("DEPRECATION") // monthNumber 无替代 API - fun getOrCompute(date: LocalDate): DayCellInfo { - cache[date]?.let { return it } + suspend fun getOrCompute(date: LocalDate): DayCellInfo = mutex.withLock { + cache[date]?.let { return@withLock it } val computed = compute(date) cache[date] = computed trimIfNeeded() - return computed + computed } /** @@ -33,8 +38,7 @@ object LunarCache { * * @param dates 日期列表 */ - @Synchronized - fun precompute(dates: List) { + suspend fun precompute(dates: List) = mutex.withLock { dates.forEach { date -> if (!cache.containsKey(date)) { cache[date] = compute(date) @@ -48,23 +52,21 @@ object LunarCache { * * 复用缓存中的 lunarMonthName 和 annotationText,避免重复创建 SolarDay。 */ - @Synchronized @Suppress("DEPRECATION") // monthNumber 无替代 API - fun formatLunarDate(date: LocalDate): String { + suspend fun formatLunarDate(date: LocalDate): String { val info = getOrCompute(date) val dayText = info.annotationText.removeSuffix("月") return "农历${info.lunarMonthName}${dayText}" } private fun trimIfNeeded() { - if (cache.size > MAX_SIZE) { - val toRemove = (cache.size * 0.2).toInt().coerceAtLeast(1) + while (cache.size > maxSize) { val iterator = cache.keys.iterator() - var removed = 0 - while (iterator.hasNext() && removed < toRemove) { + if (iterator.hasNext()) { iterator.next() iterator.remove() - removed++ + } else { + break } } } @@ -104,6 +106,11 @@ object LunarCache { } return DayCellInfo(text, false, holidayBadge, lunarMonthName) } + + companion object { + private const val MAX_SIZE = 800 + val default = LunarCache() + } } /** diff --git a/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt b/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt index 92f5285..fa33798 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/BottomCard.kt @@ -17,7 +17,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -60,7 +62,12 @@ fun BottomCard( @Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-datetime 尚未提供新接口 val solarDesc = "${selectedDate.monthNumber}月${selectedDate.day}日" - val lunarDesc = remember(selectedDate) { LunarCache.formatLunarDate(selectedDate) } + val lunarDesc by produceState( + initialValue = "", + key1 = selectedDate + ) { + value = LunarCache.default.formatLunarDate(selectedDate) + } val shiftMessage = when (viewModel.shiftKindAt(selectedDate)) { ShiftKind.WORK -> "小小上班,轻松拿下!" ShiftKind.OFF -> "耶耶耶,美美休息!" diff --git a/core/src/main/kotlin/plus/rua/project/ui/CalendarUtils.kt b/core/src/main/kotlin/plus/rua/project/ui/CalendarUtils.kt index 3482094..421d94a 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/CalendarUtils.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/CalendarUtils.kt @@ -38,6 +38,38 @@ const val CARD_GAP_COLLAPSED_DP = 12 /** 线性插值 */ fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - start) * fraction +/** + * 月份网格信息,包含计算日历网格所需的所有数据。 + */ +data class MonthGridInfo( + val year: Int, + val month: Int, + val firstOfMonth: LocalDate, + val offset: Int, + val startDate: LocalDate, + val daysInMonth: Int, + val rows: Int, + val totalDays: Int +) + +/** + * 计算指定年月的日历网格信息。 + * + * @param year 年份 + * @param month 月份(1-12) + * @return 月份网格信息 + */ +fun getMonthGridInfo(year: Int, month: Int): MonthGridInfo { + val firstOfMonth = LocalDate(year, month, 1) + val offset = firstOfMonth.dayOfWeek.ordinal + val startDate = firstOfMonth.minus(DatePeriod(days = offset)) + val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1) + val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).day + val rows = ((offset + daysInMonth - 1) / 7) + 1 + val totalDays = rows * 7 + return MonthGridInfo(year, month, firstOfMonth, offset, startDate, daysInMonth, rows, totalDays) +} + /** * 计算月份在日历网格中需要的行数(4/5/6)。 * @@ -46,11 +78,7 @@ fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - star * @return 网格行数 */ fun calculateWeeksCount(year: Int, month: Int): Int { - val firstOfMonth = LocalDate(year, month, 1) - val offset = firstOfMonth.dayOfWeek.ordinal - val nextMonth = if (month == 12) LocalDate(year + 1, 1, 1) else LocalDate(year, month + 1, 1) - val daysInMonth = nextMonth.minus(DatePeriod(days = 1)).day - return ((offset + daysInMonth - 1) / 7) + 1 + return getMonthGridInfo(year, month).rows } /** diff --git a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt index 1c976c6..2cb53ef 100644 --- a/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt +++ b/core/src/main/kotlin/plus/rua/project/ui/DayCell.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -36,6 +37,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import kotlinx.datetime.LocalDate +import plus.rua.project.DayCellInfo import plus.rua.project.LunarCache import plus.rua.project.ShiftKind @@ -66,11 +68,19 @@ fun DayCell( shiftKind: ShiftKind?, showLegalHoliday: Boolean, onClick: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + lunarCache: LunarCache = LunarCache.default ) { - val (annotationText, isAnnotationHighlight, holidayBadge) = remember(date) { - LunarCache.getOrCompute(date) + val lunarData by produceState( + initialValue = DayCellInfo("", false, null), + key1 = date, + key2 = lunarCache + ) { + value = lunarCache.getOrCompute(date) } + val annotationText = lunarData.annotationText + val isAnnotationHighlight = lunarData.isAnnotationHighlight + val holidayBadge = lunarData.holidayBadge val currentState = when { isSelected && isToday -> DayCellState.SELECTED_TODAY isSelected -> DayCellState.SELECTED