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:
parent
bf28008d17
commit
774e03a928
@ -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<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,
|
||||
|
||||
@ -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<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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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 -> "耶耶耶,美美休息!"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user