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.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 无替代 API,kotlinx-datetime 尚未提供新接口
|
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 农历/节气/节假日信息缓存。
|
* 农历/节气/节假日信息缓存。
|
||||||
*
|
*
|
||||||
* 使用 LinkedHashMap(accessOrder=true)实现 LRU 语义,读写速度优于 ConcurrentHashMap。
|
* 使用 LinkedHashMap(accessOrder=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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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 无替代 API,kotlinx-datetime 尚未提供新接口
|
@Suppress("DEPRECATION") // monthNumber 无替代 API,kotlinx-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 -> "耶耶耶,美美休息!"
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user