13 KiB
Repository Guidelines
Guide for AI assistants working in YaYa (鸭鸭日历) — a pure-Android Jetpack Compose lunar/shift calendar app. Remote: github.com/DefectingCat/yayacal (+ git.rua.plus mirror). UI text is Chinese; date logic is kotlinx-datetime only (java.util.Calendar is banned).
Note:
CLAUDE.mdis a symlink to this file.COMMENTS.mdis referenced from several docs but does not exist yet — treat the KDoc rules in Code Conventions as authoritative until it is created.
Project Overview
YaYa is a single-app Android calendar focused on: Chinese lunar calendar, solar terms (节气) and traditional festivals (via tyme4kt), a personal WORK/OFF shift cycle, month/week/year views with infinite paging, and a photo-journal ("date recorder") feature with an in-app photo editor. Latest release 1.3.0 (see CHANGELOG.md); base version lives in gradle.properties (app.version.base).
Architecture & Data Flow
Three Gradle modules (settings.gradle.kts, typesafe accessors on):
| Module | Type | Responsibility |
|---|---|---|
:core |
com.android.library |
All Compose UI, ViewModels, business logic, Room data layer |
:app |
com.android.application |
Thin shell: Activities + Manifest + theme. Iron rule: no business logic here. |
:macrobenchmark |
com.android.test |
Baseline Profile / Startup Profile generation |
Dependency chain: :app → :core; :macrobenchmark → :app.
Navigation is Activity + Intent, NOT Compose Navigation. Each screen has a 1:1 Activity in :app that just does setContent { YaYaTheme { SomeScreen() } }. core/.../ui/DateRecorderNav.kt centralizes the Intent-extra contract (EXTRA_TEMP_PHOTO_PATH, EXTRA_FINAL_PHOTO_PATH, EXTRA_RECORD_ID). Slide/fade transitions come from app/.../BaseActivity.kt.
State pattern (all ViewModels): private MutableStateFlow backing fields exposed as read-only StateFlow via asStateFlow(); many aggregate into a single uiState: StateFlow<UiState> through combine(...).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initial). The aggregation is an explicit design goal to minimize Compose recomposition. Tests read stateFlow.value directly (no Turbine).
Home-screen data flow: CalendarViewModel owns selectedDate, isCollapsed, collapseProgress (0=month↔1=week), isYearView, shiftPattern. It pulls lunar data from LunarCache (LRU LinkedHashMap + coroutine Mutex) which builds DayCellInfo per cell from tyme4kt SolarDay. CalendarMonthView lifts the shared PagerState and renders CalendarPager → CalendarMonthPage → DayCell.
Persistence: Room (DateRecordDatabase v1, exportSchema = true → core/schemas/). Repository stores relative photo paths under filesDir/Pictures/date_recorder/ and deletes photos on record delete. Non-DB prefs (shift pattern, date checker) use SharedPreferences with custom string encoding (no JSON dependency) in *Storage.kt.
Key Directories
app/src/main/
kotlin/plus/rua/project/ # Activities (shell only): MainActivity, BaseActivity, *Activity
AndroidManifest.xml # Activity registry; SplashActivity present but disabled
res/anim/, res/values/ # slide transitions, themes (Theme.Material.* NoActionBar)
core/src/main/
kotlin/plus/rua/project/ # ViewModels, business logic, data, storage, tracing
kotlin/plus/rua/project/ui/ # All Composable screens + calendar grid + nav contract
kotlin/plus/rua/project/ui/theme/ # YaYaTheme
assets/animations/*.webp # scanned at config time → BuildConfig.WEBP_FILES
baseline-prof.txt / baselineProfiles/startup-prof.txt # generated profiles
core/src/test/kotlin/plus/rua/project[/ui]/ # the only unit tests
macrobenchmark/src/main/java/.../baseline/ # StartupBenchmark, BaselineProfileGenerator
scripts/ # profile.sh, analyze-trace.sh, resize_duck_icon.py
Namespace quirk: :core's android.namespace is plus.rua.project.shared, so core's R and BuildConfig live there — but the code package is plus.rua.project.
Development Commands
./gradlew :app:assembleDebug # build debug APK
./gradlew :app:installDebug # install to device/emulator
./gradlew :core:testDebugUnitTest # all unit tests (JVM, no emulator)
./gradlew :core:testDebugUnitTest --tests "plus.rua.project.ui.CalendarUtilsTest" # one class
./gradlew spotlessApply # format (ktlint) — run before committing
Profiling & profiles (device required):
./gradlew :macrobenchmark:updateBaselineProfile # generates + copies both profiles into :core
./gradlew :macrobenchmark:connectedBenchmarkAndroidTest # benchmarks only (no copy)
./scripts/profile.sh # Perfetto trace, default 8s → logs/
./scripts/profile.sh --scenario month_browse --trace 15 # specific scenario, trace build
./scripts/profile.sh --list-scenarios # list named scenarios
./scripts/analyze-trace.sh [logs/trace_*.perfetto-trace] # post-hoc SQL analysis
Workflow
- 每完成一个功能点,提交一次。 不要攒着最后一起提交。一个"功能点"是一个可独立说明的小块改动(一个 bug 修复、一个 Composable、一组测试、一次重构),而不是整次会话的全部产出。
- 提交粒度与信息由 AI 自主决定:自行判断该功能点属于
feat/fix/refactor/docs/test/chore,写清摘要;改动小则合并提交,改动跨多步则在每步落盘。 - 只暂存本次功能点相关的文件,不要
git add .把无关改动(如会话前遗留的脏文件)一并带入。提交前看一眼git status。 - 当前在
main分支:按仓库惯例可以直接在main上提交常规改动;如改动涉及发布或需评审,先开分支。 - 每写完一个功能后跑
./gradlew :app:installDebug做一次真机/模拟器冒烟安装,验证能编过、能装上。允许失败:无连接设备、无模拟器、签名/环境问题等导致失败属正常,不必阻塞后续工作——记下失败原因即可继续。这条是为了在能装上时尽早暴露问题,不是硬性 gate。
Code Conventions & Common Patterns
- KDoc required on every public
@Composable: document parameters and when callbacks fire. (This is the missingCOMMENTS.mdcontract.) Modifieralways last in Composable signatures.- Callbacks use
onprefix (onDateClick,onDragEnd). - Clickable list items: use
Card(onClick = …)withCardDefaults.cardElevation(defaultElevation = 0.dp)— not bareModifier.clickable(). @Suppress("DEPRECATION")must carry an inline comment explaining why (currently used formonthNumber).- Dates:
kotlinx-datetimeeverywhere.java.util.Calendaris forbidden. - Testability seams:
Clockis constructor-injectable (tests pass aFixedClock); SharedPreferences are faked with in-memory impls written per-test-file (duplicated deliberately to avoid same-package private-class name clashes). - Pagers:
HorizontalPagerwithpageCount = { Int.MAX_VALUE }centered atSTART_PAGE = Int.MAX_VALUE/2; page↔month viapageToYearMonth/yearMonthToPageinui/CalendarUtils.kt; sync viasnapshotFlow { pagerState.settledPage }.drop(1). - Trace markers:
ComposeTrace.ktwrapsandroid.os.Trace, gated byBuildConfig.ENABLE_TRACE(on indebug/trace, off inrelease). Key markers:MonthView:Compose,YearView:Compose,CalendarPager:Page:<year>-<month>,WeekPager:Page,VM:collapseProgress:*,YearGridView:*. Full catalog inDEVELOPMENT.md. - Formatting: Spotless + ktlint cover
src/**/*.ktand root*.gradle.kts(not.toml/.md/scripts)..editorconfigpermits PascalCase@Composablefunction names.
Important Files
Central / load-bearing source (paths relative to repo root):
core/src/main/kotlin/plus/rua/project/CalendarViewModel.kt— home-screen state: collapse math, shift resolution, ISO week, grid generation. The heart of the app.core/src/main/kotlin/plus/rua/project/ui/CalendarMonthView.kt— home Composable: month↔year transition, FAB menu, pager wiring (largest UI file).core/src/main/kotlin/plus/rua/project/ui/CalendarUtils.kt— all page↔date math + layout constants (START_PAGE,COLLAPSE_THRESHOLD).core/src/main/kotlin/plus/rua/project/LunarCache.kt— tyme4kt integration; producesDayCellInfo(the per-cell render contract).core/src/main/kotlin/plus/rua/project/ShiftPattern.kt— shift resolution (kindAt: overrides → rephaseFlips → active-anchor cycle). Most rigorously tested file.core/src/main/kotlin/plus/rua/project/ui/CalendarPager.kt,ui/WeekPager.kt— the two infinite pagers.core/src/main/kotlin/plus/rua/project/ui/CalendarMonthPage.kt,ui/DayCell.kt— per-page grid + cell rendering with collapse animation.core/src/main/kotlin/plus/rua/project/ui/YearGridView.kt— year overview grid.core/src/main/kotlin/plus/rua/project/PhotoProcessor.kt+PhotoEditorState.kt+PhotoEditorViewModel.kt— photo editor pipeline (load/rotate/crop/render withHandStrokeoverlay).core/src/main/kotlin/plus/rua/project/{DateRecord,DateRecordDao,DateRecordDatabase,DateRecordConverters,DateRecorderRepository}.kt— Room data layer for the photo journal.core/src/main/kotlin/plus/rua/project/ComposeTrace.kt— perf tracing shim.core/src/main/kotlin/plus/rua/project/ui/DateRecorderNav.kt— Intent extra keys (cross-Activity contract).app/src/main/kotlin/plus/rua/project/{MainActivity,BaseActivity}.kt— entry point + transition base.gradle/libs.versions.toml— all dependency versions.app/build.gradle.kts— build types, dynamic versioning (baseVersion_gitHash_buildDate).
Other docs: DEVELOPMENT.md (perf/profile workflow + trace marker catalog), README.md (user intro), CHANGELOG.md (Keep-a-Changelog; [Unreleased] + [1.3.0] - 2026-07-09). Module rules: app/AGENTS.md, core/AGENTS.md, macrobenchmark/AGENTS.md.
Runtime/Tooling Preferences
- JDK 17 (
VERSION_17in all modules). Gradle wrapper 9.5.1. - AGP 9.2.1 · Kotlin 2.3.21 · KSP 2.3.10 · Compose BOM 2026.06.01. compileSdk/targetSdk 37, minSdk 24.
- Key libs: kotlinx-datetime 0.8.0 · tyme4kt 1.5.0 · sketch 4.4.0 · zoomimage 1.6.0 · Room 2.8.4 · lifecycle 2.11.0 · activity-compose 1.13.0 · CameraX 1.5.3 · Media3 1.6.1.
- Gradle caches on: configuration cache + build cache + parallel (
gradle.properties). R8 full mode enabled. - Build types:
debug(default, trace on) ·release(R8 + resource shrink, trace off, debug-signed) ·trace(release + trace markers) ·benchmark(release base, no minify — so generated profile names aren't obfuscated). - Profiling needs a device/emulator with GPU acceleration (software renderer can't produce
gfxinfoframestats). Real benchmarks need a physical device on a release target. - No CI/CD exists — all builds, tests, profiling, and releases run locally.
Testing & QA
- All automated tests are pure-JVM unit tests in
core/src/test/kotlin/plus/rua/project[/ui]/— noandroidTestsource set, no Compose UI tests, no Robolectric/Turbine/Mockk. Run on JVM 17, no emulator. - Frameworks:
kotlin-test-junit(+ JUnit 4 in a few classes) andkotlinx-coroutines-test(runTest).androidx.room:room-testingis declared but unused. - Covered well: date math (
CalendarUtilsTest), the shift engine (ShiftPatternTest— cycle/override/RephaseFlip),CalendarViewModelobservable state + grid generation, storage round-trips, record sorting, lunar birthday/rose-day flags,PhotoProcessor.calculateInSampleSize,HandStrokesegmentation. - Coverage gaps to be aware of: no Compose UI/instrumented tests (UI exercised only via the macrobenchmark journey); Room DAO/Database/migration logic untested;
Flowemission sequences untested (only synchronous.valuesnapshots);DateRecordDetailViewModel/PhotoEditorViewModeland the edit-record path ofRecordEditViewModelhave no dedicated tests. - Test conventions: classes
*Test.kt; methodsmethod_condition_result; in-memory SharedPreferences fakes written per file; fixtures inline (no shared helpers). Comments/KDoc are in Chinese and state scope explicitly. - Macrobenchmark (
macrobenchmark/src/main/.../baseline/) provides the only on-device coverage —StartupBenchmark(cold start) andBaselineProfileGenerator(drives a 12-step user journey to emitbaseline-prof.txt+startup-prof.txt).
Release Process
Driven by the yayacal-release skill (.agents/skills/yayacal-release/SKILL.md). Flow: bump app.version.base in gradle.properties + versionCode in app/build.gradle.kts → update CHANGELOG.md from git log <last-tag>..HEAD → commit exactly release: vx.y.z → lightweight tag vx.y.z → push main --tags → assembleRelease → gh release create with extracted notes. Release only from clean main; keep debug signing (no release keystore). Note: the skill's doc links to github.com/xfy/yayacal — the actual remote is DefectingCat/yayacal.