Start here

ADR-0002: Android as a second native client

Status: accepted · 2026-10-18

Context

ADR-0001 fixed the client side to native iOS. Apps built from botgumi also need to ship on Android, natively, with the same provider interfaces, the same agent team and the same gates. The Swift package already shows the shape: interfaces, no-ops and contract suites per concern, vendor code only in adapters. Android needs the same shape in Kotlin, a track of its own, a store path, and CI.

Decisions

  1. Two native client platforms. Android joins iOS in the opinionated core (this amends ADR-0001, decision 5). Android apps are native: Kotlin, Jetpack Compose, Gradle with the Kotlin DSL and a version catalog. No cross-platform UI layer. An app lists its client platforms in botgumi.config.ts (platforms, default ["ios", "android"]). Each platform is its own track (ios/, android/) with its own worker, CI status and store checklist; the generator creates only the chosen tracks.
  2. One Gradle build under packages/kotlin/. Gradle needs no manifest at the repository root, so the Kotlin build lives next to the Swift sources (the Swift manifest stays at the root for SwiftPM's sake). Modules mirror the Swift modules one to one: botgumi-auth, botgumi-api-client, botgumi-design-tokens, botgumi-analytics (Analytics and Flags), botgumi-analytics-amplitude, botgumi-crash, botgumi-crash-sentry, botgumi-purchases, botgumi-purchases-revenuecat, botgumi-contracts. Group dev.botgumi, artifact = module name, version = the git tag, the same version as the Swift package. Apps depend on the Maven artifacts; the publish step lands with the first tagged release, so until then an app uses includeBuild on a checkout. The coordinates are fixed now so app code never changes.
  3. Interface modules are plain Kotlin/JVM. They target JVM 17, depend on nothing but the Kotlin standard library, and use suspend where Swift uses async. So they build and test with any JDK 17+ and no Android SDK, here and on Linux CI. Adapter modules are stubs on the same footing today; each becomes an Android library (com.android.library, minSdk 26) when its vendor SDK lands, since those SDKs need Android. Only an adapter module may import a vendor SDK; the vendor-import lint covers Kotlin imports too.
  4. Sign-in on Android is Sign in with Google through Credential Manager. GoogleSignIn returns a GoogleCredential (the ID token and the request nonce); SessionExchange sends it to the backend, which verifies it and returns the app session, as it does for an Apple credential. The backend's session-exchange endpoint accepts both providers; an app that turns Android on records that in docs/api/openapi.yaml. The session store is Android Keystore-backed in production, in memory in tests.
  5. Strings. res/values/strings.xml holds en and res/values-b+zh+Hans/strings.xml holds zh-Hans (the BCP 47 qualifier matches the zh-Hans locale id used everywhere else). The Android CI fails when a key is missing in either, as the iOS CI does for the string catalog.
  6. CI. Kotlin and Android run on GitHub's Linux runners, which are cheap, unlike macOS. This repo's CI gets a kotlin job (./gradlew build in packages/kotlin), and pnpm verify runs it locally too, so a JDK 17+ is a requirement of this repo. Each app's Android CI is a GitHub workflow that runs android/scripts/ci.sh (lint, unit tests, strings check) and reports as the android-ci status. iOS stays local (ADR-0001, 10).
  7. Store. Google Play, through the Play Developer API (the Gradle Play Publisher plugin). Builds go to the internal testing track first; release signing is Play App Signing, with the upload key in the secrets inventory. Human gate 7 becomes store submission and covers both stores. Play gets its own asset and compliance checklists (docs/checklists/google-play-*.md). The dashboard reads Play data through a @botgumi/google-play adapter, the twin of @botgumi/asc.
  8. Design tokens get a third output. generateKotlin writes DesignTokens.kt into botgumi-design-tokens from the same source as the CSS and Swift outputs.
  9. Merge policy. The required statuses include each chosen platform's CI status: ios-ci when iOS is on, android-ci when Android is on, plus botgumi/review.

Consequences

  • Every provider with a client side is now implemented twice (Swift and Kotlin). The contract suites are the same in both, so an adapter is done when it passes the suite on its platform.
  • The Android SDK is needed only to build adapters with vendor SDKs and the apps themselves, not this repo.
  • Kotlin packages get GitHub CI, which Swift does not have; the Swift build stays a local check.
  • The backend worker owns the Google credential path of the session exchange; it is a contract change, so it goes through a change request with both reviewers.
  • @botgumi/i18n's checkCatalog has to read strings.xml as well as .xcstrings exports.
  • The number of tracks grows to seven. The orchestrator opens one issue per track as before, so an Android-only or iOS-only app has one fewer.