Proposal: Android (Kotlin / Jetpack Compose) guidance surface + twin native development #17
rterakedis
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Proposal: Android (Kotlin / Jetpack Compose) guidance surface + twin native development
Status: idea for discussion — nothing implemented yet.
Motivation
Leanwheel currently ships two platform guidance surfaces: Swift/SwiftUI (
stubs/swift/,refresh-swift,swift-audit,appstore-preflight,sim.sh) and Web (stubs/web/,refresh-web,web-audit). This proposal adds a third — native Android (Kotlin + Jetpack Compose) — and, on top of it, a twin-development mode: building the Apple and Android versions of the same app side-by-side as two fully native codebases driven by one shared spec.Why two native codebases instead of a cross-platform layer (KMP, React Native, Flutter)? Those layers reintroduce exactly the problems a native-first developer wants to avoid: interop quirks, debugging across a bridge, a third toolchain. The historical objection to "write it twice natively" was human cost — but with agents, the cost structure flips. The expensive artifact is the spec (Behavior Contract, ACs, edge cases, design tokens), and leanwheel already treats the spec as the first-class deliverable. Two dev agents implementing the same contract natively is cheaper and safer than one agent translating idioms between platforms.
Leanwheel is already shaped for this: the flywheel is an orchestrator holding a small context that spawns isolated dev subagents returning terse reports. Twin dev is the same shape with two spawns per story instead of one. The real risks are spec drift (platforms quietly diverging in behavior) and doubled verification burden — both of which have leanwheel-native mitigations (shared Behavior Contracts, the stack-agnostic
evalscommand cases, a parity pass at review).Scope decisions (proposed)
ViewModel+StateFlow+ unidirectional data flow), and Compose's lifecycle/config-change model genuinely wants ViewModels. The Android stubs should follow the platform and record the asymmetry explicitly so a future sync doesn't "fix" it./setupalready supports multi-surface projects (web + Apple in one repo, both guardrail blocks in one CLAUDE.md, both stub trees underdocs/setup/). Android becomes a third surface in that same pattern. The shared spec then lives in the monorepo'sdocs/for free: single git history (atomic cross-platform commits per story), one GitHub issue/milestone set, and worktree isolation already handles two dev agents working different subtrees of the same repo.Phase 0 — Facts pass before authoring anything
The
refresh-swifthistory shows why this comes first: a past run invented a phantom OS version, and the simulator-automation work proved toolchain facts must be verified live, not recalled. Before writing stubs:/research-style cited pass on current-stable facts: Android API level and target-SDK policy, Kotlin 2.x / Compose BOM versions, Play Console policies.appstore-preflighttreatment: dated,Phase 1 — Guidance stubs (
setup/stubs/android/+modern-android.md)Mirror the Swift stub set file-for-file where an analogue exists, authored via the research pass, in the same lean ✅/❌ style:
architecture.mdstate-management.mdStateFlow/remember/ state hoisting /SavedStateHandleconcurrency.mdviewModelScopeui-composition.mdswiftdata.mdpersistence.md: Room + DataStore (self-gating, likeswiftdata.md)testing.mdtestability.mdModifier.testTagconvention (theaccessibilityIdentifieranalogue), one deep-link route tableaccessibility.mdanti-patterns.mdsimulator.mdemulator.md— see Phase 3modern-swiftui.mdmodern-android.mdguardrails block (~50 lines,## Android Guardrailsheading in project CLAUDE.md)Encouragingly, the deep-link route-table principle ports cleanly and Android's tooling is friendlier than
simctl:adb shell am start -ddelivers custom-scheme links without the iOS 26 "Open in app?" alert,uiautomator dumpgives a real view hierarchy without a test runner, and dark mode / font scale are one-lineadb shellsettings.Phase 2 — Skill kit (mirroring the four-skill Swift pattern)
refresh-android— mirrorsrefresh-swift: gold-standard sources (developer.android.com, the Now in Android sample, Compose docs, Kotlin docs, pinned community authors), version-axis rules, updatesdocs/setup/android/+ stubs, and refreshes the Play-facts tables in the preflight skill (cheap-gated on Google's policy changelog).android-audit— mirrorsswift-audit: greps source/docs/stories against the guidance,testTagconvention check, staged testability retrofit (Stage 0 foundation + per-screen stages), Pass-F deletion tags, one remediation story out.playstore-preflight— mirrorsappstore-preflight, and for a first-time Android publisher it's the highest-value skill in the set: audits manifest permissions vs declared usage, target SDK floor, Data Safety consistency, signing config, AAB readiness — plus the non-code checklist (Play Console account, identity verification, closed-testing gates, content rating questionnaire, store listing assets, pre-launch report).setupgains an Android scaffold question + stub-copy step + guardrails append;upgrade-projectdetects the new assets;dev-storystep 4 routesdocs/setup/android/files by story topic;code-reviewreads them as rejection criteria;epics/check-readinessCheck 8's testability-foundation-story requirement generalizes from Apple-only to Apple-or-Android;discover/nextdetect Android projects (settings.gradle.kts);guard-a11y-id.shlearns the Compose interactive-element /testTagpattern, gated on the Android testability stub existing.Per-story platform routing (forced by the monorepo layout)
Today's model routing is per-project:
swift_project= "an.xcodeprojexists anywhere" → Opus for every dev story. In a monorepo holding both apps, an Android story would falsely route as Swift. The twin-era rule: classify each story by the platform/subtree it touches (frontmatterplatform:field, or derived from the Design Contract), and route model + guidance stubs + Build & Test Gate command from that —xcodebuildfor Apple stories,./gradlewfor Android stories, both for twin stories. Guidance routing stays scoped the same way: a story loadsdocs/setup/swift/ordocs/setup/android/, never both.Model routing for Kotlin: start on the Sonnet default (Kotlin has far more training data and better compiler diagnostics than Swift) and let
docs/metrics/flywheel-ledger.jsonlbuild-iteration counts show whether it needs the Swift-style Opus exception. That's exactly what the ledger exists for.Phase 3 — Emulator automation (
emu.sh+ live verification)The
sim.shanalogue, scaffolded by/setupon Android projects:doctor(JDK,ANDROID_HOME, sdkmanager, licenses accepted, an AVD exists) /boot/install/launch/shots(dark/light × font-scale matrix viaadb shellsettings, one boot per device) /dump(uiautomator) /flow(Compose UI tests or Gradle managed devices) /open(deep links viaam start)..leanwheel/emu.json— machine-independent by construction, artifacts self-ignoring: the same conventionssim.jsonsettled on.design-verifyand thedev-storyBuild & Test Gate get Android branches (./gradlew assembleDebug testDebugUnitTest).This phase must end with a live verification session on a real machine: install the command-line SDK, create an arm64 AVD, and actually run every subcommand. The
sim.shregression notes exist because two silent failures only surfaced under real use — the same discipline applies here.Phase 4 — Twin native development (the genuinely new part)
Ships after Phases 1–3 are proven on a standalone Android project, because it composes them.
docs/. Each platform subtree keeps its own architecture doc, platform Design Contract sections, story task lists, and platform-specific evalcommandlines. The Behavior Contract is the keystone: it's already defined as flows/states/invariants/edge cases with no platform vocabulary, so one contract legitimately drives two implementations.twin-storyorchestrator): create-story runs once, producing the shared contract + two platform Design Contracts; then twolw-story-developerspawns, one per subtree; each runs its own Build & Test Gate and evals; review adds a parity pass — same ACs checked off on both platforms, same edge-case handling, invariants verified on both sides. A story isn't done until both platforms are green, so drift can't accumulate silently.Data sync: within-ecosystem vs cross-ecosystem
This is the one place twin development forks on a product decision rather than a framework one, and the guidance should force the question early (at
/prdorcheck-readinesstime, before twin epics are planned):NSPersistentCloudKitContainer); the Android app uses Room locally (+ optionally Android's own backup / Drive auto-backup). Parity then lives at the schema and behavior level: the Behavior Contract gains a data-shape section so the Room schema mirrors the Core Data / SwiftData model entity-for-entity, and migration discipline is enforced on both sides. Cheap, and it doesn't touch a working Apple app.Supporting stub work either way:
persistence.mdgets a "Room mirrors the source schema" section for twin projects, and the Android testability stub gets the seeding-guard analogue of the Swift CloudKit guard (never seed fixtures into a production-synced store).Play Store onboarding facts (dated Aug 2026 —⚠️ volatile, re-verify)
Captured here because they carry real lead time for a first-time publisher; they'd live in
playstore-preflight's non-code checklist, refreshed byrefresh-android:Sources: Google Play developer community — org account clarification, The Google Play 12-Tester Rule, Explained (2026), Google Play Closed Testing Explained
Proposed rollout
stubs/android/+modern-android.md→refresh-android/android-audit/playstore-preflight→ skill wiring with per-story platform routing →emu.sh+emulator.md, ending with the live toolchain verification session. Symlink sync at the end (new skills/agents).emu.sh→ preflight) — doubles as an Android on-ramp for a maintainer who's never shipped one.Open questions
platform:frontmatter vs deriving platform from the Design Contract — which is the cleaner routing key?--platforms apple,android) or a separate thin orchestrator skill?All reactions