quartz and quic now come from Maven Central (libs.quartz, libs.quic), but a few module docs and skills still described them as Gradle modules here: - marmotQuic/README.md, nestsClient/tests/README.md: `:quic` -> quic-kmp - cli/DEVELOPMENT.md, commonsUI/ARCHITECTURE.md: `:quartz` -> libs.quartz - amy-expert / kotlin-multiplatform extraction recipes: protocol code is a quartz-repo PR (built here with -PquartzDir), not a git mv into quartz/ - AGENTS.md Tech Stack and the kotlin-multiplatform skill: the app modules moved to kotlinx.serialization; Jackson remains only inside quartz's JVM code, so the "don't migrate to kotlinx.serialization" rule is reversed - BUILDING.md: the currentTimeMillis ban is verifyKmpPurity, not a Spotless rule
5.6 KiB
Extract-from-Android recipe
The single most common reason an Amy feature request stalls: the
logic it needs lives in amethyst/ with Android-only imports. You
cannot call it from cli/. You have to move it first.
This recipe is how.
When to extract
Before writing a new command, ask:
-
Does the piece of logic I need exist in
quartz/orcommons/?- Yes → use it.
- No, but it's in
amethyst/→ extract. This file. - No, it doesn't exist anywhere → design it in
commons/directly. Write a plan doc undercommons/plans/if it's a new subsystem.
-
Never duplicate
amethyst/logic intocli/. That's a debt you will pay later when the Android caller drifts.
Recipe
Land this as its own commit, before the commit that adds the CLI command.
Step 1 — Find the class
grep -rn "class <ClassName>" amethyst/src/main/java/
Identify the minimum unit to move. Sometimes it's a whole file, sometimes one function. Prefer the smallest unit that makes the command possible.
Step 2 — List Android-only dependencies
Walk the imports. The usual offenders:
| Dependency | Treatment |
|---|---|
android.content.Context |
Often accidental — inline if only used for logging or preferences. Otherwise, invert as constructor arg. |
android.content.SharedPreferences |
Abstract behind an interface in commons/; Android actual uses SharedPreferences, JVM actual uses a JSON file. |
androidx.work.WorkManager |
Rarely shareable — if the CLI needs it, simplify the flow to not require background scheduling. |
android.util.Log |
Replace with Quartz Log (quartz/…/utils/Log.kt), lambda overload for interpolated messages. |
android.graphics.Bitmap |
Almost never needed by Amy. Keep in Android and split the function. |
android.net.Uri |
Replace with kotlinx.io path types or a plain String. |
androidx.compose.* |
Compose UI (ui/foundation/material3), Coil and Res must stay out of commons entirely — they belong in :commonsUI, which Amy never depends on. Only the Compose runtime (@Stable, snapshot state) is allowed in commons. |
Step 3 — Pick a migration strategy per dependency
- Inline-able. One call, trivial. Delete it.
- Platform-abstractable. Add
expectincommons/commonMain/+actualincommons/androidMain/+actualincommons/jvmMain/. See thekotlin-multiplatformskill for the mechanics and for thejvmAndroidsource-set pattern used throughout this repo. - Inversion-of-control. Take the Android dependency as a constructor arg with an interface type. Amy supplies a JVM flavour; Android supplies the Context-backed one.
Step 4 — Move the code
# Target location depends on what it is:
# - Protocol → a PR to vitorpamplona/quartz (quartz/src/commonMain/kotlin/… there),
# built here with -PquartzDir=../quartz until it ships; not a git mv in this repo
# - Business logic → commons/src/commonMain/kotlin/…
# - UI → commonsUI/src/commonMain/… (needs Compose Multiplatform; never used by amy)
git mv amethyst/src/main/java/com/.../<ClassName>.kt \
commons/src/commonMain/kotlin/com/.../<ClassName>.kt
Update package declarations. Run ./gradlew spotlessApply.
Step 5 — Update the Android caller
The amethyst/ caller now imports from the new location. Often this is the only code change visible in the Android app.
If the Android caller was using a concrete Android-backed dependency, it now supplies that concrete dependency explicitly.
Step 6 — Add a JVM test
In commons/src/commonTest/kotlin/… or commons/src/jvmTest/kotlin/…
(depending on what the code exercises), add a test that runs on JVM.
This guards against Android-only imports sneaking back in, and it's
the only way to be sure Amy can now call the code.
Step 7 — Commit
Single commit, descriptive:
refactor(<area>): extract <ClassName> to commons for CLI reuse, with a
body naming the old and new location, how each Android dependency was
handled (inlined, expect/actual, or inverted behind an interface), and
"No behaviour change on Android."
Step 8 — Now add the CLI command
Separate commit. Follows "How to add a command" in cli/DEVELOPMENT.md.
Cautionary notes
- Don't extract speculatively. Only extract what the current command needs. A feature-complete port can happen later; right now the goal is to unblock one command without adding surface area you don't have a second caller for.
- Android is allowed to keep side-effects. Notifications,
background services, Intents, camera, permissions dialogs — those
stay in
amethyst/. Amy's job isn't to replicate UX, it's to exercise the protocol underneath. - Check the consumers. Sometimes the "logic" you want is already
partially in
commons/, and theamethyst/class is just a thin wrapper. In that case, re-use thecommons/class directly and delete the wrapper or keep it if Android genuinely needs it. - Tests first if you're nervous. Copy the existing
amethyst/-side test (if any), make it JVM-only by removing Android imports, and watch it pass after the move.
Red flags during extraction
Stop and reconsider if:
- The Android class is 1000+ lines. Extract only the piece the CLI needs; leave the rest for a follow-up.
- You need
Contextin 30 places. It's probably being used as a grab-bag; sort by actual use (strings, preferences, services, …) and abstract those individually. - You find yourself writing
expect classwith a dozen methods. A fine-grained interface is usually clearer than a monolithic expect-actual. - You're about to add a Compose import to
cli/. Stop.