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.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 无替代 APIkotlinx-datetime 尚未提供新接口
fun getMonthDays(year: Int, month: Int): List<CalendarDay> {
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,

View File

@ -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
/**
* 农历/节气/节假日信息缓存
*
* 使用 LinkedHashMapaccessOrder=true实现 LRU 语义读写速度优于 ConcurrentHashMap
* 通过 @Synchronized 保护并发访问冷启动时主线程单线程访问偏向锁使其几乎零开销
* 使用 LinkedHashMapaccessOrder=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<LocalDate, DayCellInfo>(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<LocalDate>) {
suspend fun precompute(dates: List<LocalDate>) = 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()
}
}
/**

View File

@ -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 无替代 APIkotlinx-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 -> "耶耶耶,美美休息!"

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
/**
* 月份网格信息包含计算日历网格所需的所有数据
*/
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
}
/**

View File

@ -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