refactor: Wave 2 — LunarCache 可注入化 + 重复计算提取

- 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) <noreply@anthropic.com>
This commit is contained in:
xfy 2026-05-21 17:58:35 +08:00
parent bf28008d17
commit 774e03a928
5 changed files with 89 additions and 51 deletions

View File

@ -23,6 +23,7 @@ import kotlinx.datetime.todayIn
import plus.rua.project.LunarCache import plus.rua.project.LunarCache
import plus.rua.project.ui.COLLAPSE_THRESHOLD import plus.rua.project.ui.COLLAPSE_THRESHOLD
import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP import plus.rua.project.ui.FLING_VELOCITY_THRESHOLD_DP
import plus.rua.project.ui.getMonthGridInfo
import kotlin.time.Clock import kotlin.time.Clock
/** /**
@ -64,26 +65,19 @@ class CalendarViewModel(
currentMonth to currentYear, currentMonth to currentYear,
currentMonth + 1 to currentYear currentMonth + 1 to currentYear
).map { (month, year) -> ).map { (month, year) ->
when { val (normalizedMonth, normalizedYear) = when {
month < 1 -> 12 to year - 1 month < 1 -> 12 to year - 1
month > 12 -> 1 to year + 1 month > 12 -> 1 to year + 1
else -> month to year else -> month to year
} }
getMonthGridInfo(normalizedYear, normalizedMonth)
} }
monthsToPrecompute.forEach { (month, year) -> monthsToPrecompute.forEach { info ->
val firstOfMonth = LocalDate(year, month, 1) val dates = (0 until info.totalDays).map { i ->
val offset = firstOfMonth.dayOfWeek.ordinal info.startDate.plus(DatePeriod(days = i))
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))
} }
LunarCache.precompute(dates) LunarCache.default.precompute(dates)
} }
} }
} }
@ -327,17 +321,9 @@ class CalendarViewModel(
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口 @Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
fun getMonthDays(year: Int, month: Int): List<CalendarDay> { fun getMonthDays(year: Int, month: Int): List<CalendarDay> {
composeTraceBeginSection("getMonthDays:$year-$month") composeTraceBeginSection("getMonthDays:$year-$month")
val firstOfMonth = LocalDate(year, month, 1) val info = getMonthGridInfo(year, month)
val dayOfWeekOffset = firstOfMonth.dayOfWeek.ordinal val result = (0 until info.totalDays).map { i ->
val startDate = firstOfMonth.minus(DatePeriod(days = dayOfWeekOffset)) val date = info.startDate.plus(DatePeriod(days = i))
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))
CalendarDay( CalendarDay(
date = date, date = date,
isCurrentMonth = date.month.number == month && date.year == year, isCurrentMonth = date.month.number == month && date.year == year,

View File

@ -1,31 +1,36 @@
package plus.rua.project package plus.rua.project
import com.tyme.solar.SolarDay import com.tyme.solar.SolarDay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
/** /**
* 农历/节气/节假日信息缓存 * 农历/节气/节假日信息缓存
* *
* 使用 LinkedHashMapaccessOrder=true实现 LRU 语义读写速度优于 ConcurrentHashMap * 使用 LinkedHashMapaccessOrder=true实现 LRU 语义
* 通过 @Synchronized 保护并发访问冷启动时主线程单线程访问偏向锁使其几乎零开销 * 通过 [Mutex] 保护并发访问协程友好不阻塞线程
*
* @param maxSize 缓存最大容量默认 800
*/ */
object LunarCache { class LunarCache(
private const val MAX_SIZE = 800 private val maxSize: Int = MAX_SIZE
) {
private val mutex = Mutex()
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private val cache = LinkedHashMap<LocalDate, DayCellInfo>(256, 0.75f, true) private val cache = LinkedHashMap<LocalDate, DayCellInfo>(256, 0.75f, true)
/** /**
* 获取指定日期的信息缓存 miss 同步计算 * 获取指定日期的信息缓存 miss 计算
*/ */
@Synchronized
@Suppress("DEPRECATION") // monthNumber 无替代 API @Suppress("DEPRECATION") // monthNumber 无替代 API
fun getOrCompute(date: LocalDate): DayCellInfo { suspend fun getOrCompute(date: LocalDate): DayCellInfo = mutex.withLock {
cache[date]?.let { return it } cache[date]?.let { return@withLock it }
val computed = compute(date) val computed = compute(date)
cache[date] = computed cache[date] = computed
trimIfNeeded() trimIfNeeded()
return computed computed
} }
/** /**
@ -33,8 +38,7 @@ object LunarCache {
* *
* @param dates 日期列表 * @param dates 日期列表
*/ */
@Synchronized suspend fun precompute(dates: List<LocalDate>) = mutex.withLock {
fun precompute(dates: List<LocalDate>) {
dates.forEach { date -> dates.forEach { date ->
if (!cache.containsKey(date)) { if (!cache.containsKey(date)) {
cache[date] = compute(date) cache[date] = compute(date)
@ -48,23 +52,21 @@ object LunarCache {
* *
* 复用缓存中的 lunarMonthName annotationText避免重复创建 SolarDay * 复用缓存中的 lunarMonthName annotationText避免重复创建 SolarDay
*/ */
@Synchronized
@Suppress("DEPRECATION") // monthNumber 无替代 API @Suppress("DEPRECATION") // monthNumber 无替代 API
fun formatLunarDate(date: LocalDate): String { suspend fun formatLunarDate(date: LocalDate): String {
val info = getOrCompute(date) val info = getOrCompute(date)
val dayText = info.annotationText.removeSuffix("") val dayText = info.annotationText.removeSuffix("")
return "农历${info.lunarMonthName}${dayText}" return "农历${info.lunarMonthName}${dayText}"
} }
private fun trimIfNeeded() { private fun trimIfNeeded() {
if (cache.size > MAX_SIZE) { while (cache.size > maxSize) {
val toRemove = (cache.size * 0.2).toInt().coerceAtLeast(1)
val iterator = cache.keys.iterator() val iterator = cache.keys.iterator()
var removed = 0 if (iterator.hasNext()) {
while (iterator.hasNext() && removed < toRemove) {
iterator.next() iterator.next()
iterator.remove() iterator.remove()
removed++ } else {
break
} }
} }
} }
@ -104,6 +106,11 @@ object LunarCache {
} }
return DayCellInfo(text, false, holidayBadge, lunarMonthName) return DayCellInfo(text, false, holidayBadge, lunarMonthName)
} }
companion object {
private const val MAX_SIZE = 800
val default = LunarCache()
}
} }
/** /**

View File

@ -17,7 +17,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -60,7 +62,12 @@ fun BottomCard(
@Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口 @Suppress("DEPRECATION") // monthNumber 无替代 APIkotlinx-datetime 尚未提供新接口
val solarDesc = "${selectedDate.monthNumber}${selectedDate.day}" 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)) { val shiftMessage = when (viewModel.shiftKindAt(selectedDate)) {
ShiftKind.WORK -> "小小上班,轻松拿下!" ShiftKind.WORK -> "小小上班,轻松拿下!"
ShiftKind.OFF -> "耶耶耶,美美休息!" ShiftKind.OFF -> "耶耶耶,美美休息!"

View File

@ -38,6 +38,38 @@ const val CARD_GAP_COLLAPSED_DP = 12
/** 线性插值 */ /** 线性插值 */
fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - start) * fraction 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 * 计算月份在日历网格中需要的行数4/5/6
* *
@ -46,11 +78,7 @@ fun lerp(start: Float, end: Float, fraction: Float): Float = start + (end - star
* @return 网格行数 * @return 网格行数
*/ */
fun calculateWeeksCount(year: Int, month: Int): Int { fun calculateWeeksCount(year: Int, month: Int): Int {
val firstOfMonth = LocalDate(year, month, 1) return getMonthGridInfo(year, month).rows
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
} }
/** /**

View File

@ -18,6 +18,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.unit.sp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import plus.rua.project.DayCellInfo
import plus.rua.project.LunarCache import plus.rua.project.LunarCache
import plus.rua.project.ShiftKind import plus.rua.project.ShiftKind
@ -66,11 +68,19 @@ fun DayCell(
shiftKind: ShiftKind?, shiftKind: ShiftKind?,
showLegalHoliday: Boolean, showLegalHoliday: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
lunarCache: LunarCache = LunarCache.default
) { ) {
val (annotationText, isAnnotationHighlight, holidayBadge) = remember(date) { val lunarData by produceState(
LunarCache.getOrCompute(date) 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 { val currentState = when {
isSelected && isToday -> DayCellState.SELECTED_TODAY isSelected && isToday -> DayCellState.SELECTED_TODAY
isSelected -> DayCellState.SELECTED isSelected -> DayCellState.SELECTED