playstore-kmp

📁 tacuchi/playstore-kmp 📅 4 days ago
2
总安装量
2
周安装量
#74327
全站排名
安装命令
npx skills add https://github.com/tacuchi/playstore-kmp --skill playstore-kmp

Agent 安装分布

trae 2
gemini-cli 2
antigravity 2
claude-code 2
windsurf 2
github-copilot 2

Skill 文档

Play Store Release — Kotlin Multiplatform

Build a signed Android App Bundle (AAB) from a KMP project, ready for Google Play Store.

Before You Start — Assess the KMP Project Structure

KMP release is fundamentally a multi-module problem. Assess the structure BEFORE touching any files:

  1. Which is the Android module? Look for the directory containing an android {} block with applicationId:

    • composeApp/ → Compose Multiplatform template (KMP Wizard default)
    • androidApp/ → Classic KMP template
    • Custom name → Read settings.gradle.kts for include(":moduleName")
    • This module name determines: build command, output path, and AAB filename
  2. Shared module structure? Check for modules that the Android module depends on:

    • Has android {} with libraryNamespace → It’s a KMP library module; needs consumerProguardFiles
    • No android {} → Pure common code; no ProGuard concerns from this module
    • Multiple shared modules → Each one with reflection-heavy deps needs its own consumer rules
  3. Variant alignment? Check if shared/library modules define release build type:

    • Has release build type → Variants will match, no action needed
    • Missing release → Android module fails with “Could not resolve :shared variant”
    • Fix: matchingFallbacks += listOf("release", "debug") in consuming module
  4. KMP libraries in use? Check shared module dependencies:

    • Ktor → Needs ProGuard rules (engine loaded via ServiceLoader)
    • kotlinx.serialization → Needs rules (R8 strips generated serializers)
    • kotlinx.datetime → Needs rules (R8 strips JVM implementation classes)
    • Compose Multiplatform UI → Generally safe (Compose compiler handles it)
  5. Existing keystore? Ask the user before generating a new one

    • Already has .jks → Reuse it, skip Step 1
    • First release → Generate new keystore
  6. AGP + Kotlin version compatibility? Check libs.versions.toml:

    • AGP and Kotlin plugin versions must be compatible (see Kotlin compatibility matrix)
    • AGP 8+ requires Java 17

Workflow

Step Action Key file
1 Generate upload keystore upload-keystore.jks
2 Create credentials file keystore.properties
3 Configure signing in Android module <module>/build.gradle.kts
4 Configure ProGuard / R8 (by module) <module>/proguard-rules.pro + shared/consumer-rules.pro
5 Build release AAB CLI
6 Verify output CLI + checklist

Step 1 — Generate Upload Keystore

keytool -genkeypair \
  -alias upload \
  -keyalg RSA -keysize 2048 \
  -validity 10000 \
  -storetype PKCS12 \
  -keystore upload-keystore.jks

Critical details:

  • -validity 10000 = ~27 years. Google requires validity beyond Oct 22 2033.
  • -storetype PKCS12 — avoids JKS migration warnings. But with PKCS12, store password and key password must be identical. keytool silently uses the store password for the key. Different passwords → signing fails later with misleading “Cannot recover key” error.
  • Store the .jks outside the project. Recommended: ~/.android/keystores/ or a secrets manager.

Step 2 — Create Credentials File

Create keystore.properties in the project root (must NOT be committed):

storePassword=<password>
keyPassword=<same-password-as-store>
keyAlias=upload
storeFile=<absolute-or-relative-path-to-upload-keystore.jks>

Add to .gitignore:

keystore.properties
*.jks
*.keystore
local.properties

Step 3 — Configure Signing in Gradle

KMP is Kotlin DSL only (.gradle.kts). Claude knows Gradle signing config syntax. These are the KMP-specific traps:

  • Module location: The android {} block lives in the Android module (composeApp/ or androidApp/), NOT in root build.gradle.kts. KMP root build file typically only applies plugins.
  • rootProject.file() scope: keystore.properties is in project root. From within composeApp/build.gradle.kts, rootProject.file() correctly resolves to root. project.file() would look inside composeApp/.
  • signingConfigs before buildTypes: Gradle evaluates blocks in declaration order. Reference before declaration → build error.
import java.util.Properties
import java.io.FileInputStream

val keystoreProperties = Properties().apply {
    val file = rootProject.file("keystore.properties")
    if (file.exists()) load(FileInputStream(file))
}

android {
    signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = file(keystoreProperties["storeFile"] as String)
            storePassword = keystoreProperties["storePassword"] as String
        }
    }
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            signingConfig = signingConfigs.getByName("release")
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Step 4 — ProGuard / R8 (By Module)

R8 is NOT enabled by default — you must set isMinifyEnabled = true. KMP has a critical multi-module nuance: rules must be in the right module.

Where rules go:

Rule location Applies to Use for
composeApp/proguard-rules.pro Only composeApp’s own code App-level rules, signing config
shared/consumer-rules.pro Propagated to any module that depends on shared Library code using reflection (Ktor, serialization)

The Android module’s proguard-rules.pro does NOT apply to library modules. If shared/ uses Ktor or kotlinx.serialization, those rules MUST go in shared/consumer-rules.pro:

// In shared/build.gradle.kts
android {
    defaultConfig {
        consumerProguardFiles("consumer-rules.pro")
    }
}

Rules by KMP dependency (add only what applies):

# Kotlin (always needed with minification)
-keep class kotlin.Metadata { *; }
-dontwarn kotlin.**

# Coroutines
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembers class kotlinx.coroutines.** { volatile <fields>; }

# kotlinx.serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class kotlinx.serialization.json.** { *** Companion; }
-keep,includedescriptorclasses class **$$serializer { *; }
-keepclassmembers class * { @kotlinx.serialization.Serializable *; }

# Ktor Client (engine loaded via ServiceLoader — R8 strips it)
-keep class io.ktor.** { *; }
-dontwarn io.ktor.**
-keep class io.ktor.client.engine.** { *; }

# kotlinx.datetime (JVM implementation classes stripped by R8)
-keep class kotlinx.datetime.** { *; }
-dontwarn kotlinx.datetime.**

Shared Module Variant Mismatch — The #1 KMP Build Trap

Error: Could not resolve :shared or No matching variant of :shared was found

This happens when the shared module doesn’t define a release build type but the Android module tries to resolve one for bundleRelease.

Fix in the consuming module (preferred — doesn’t require modifying shared):

// composeApp/build.gradle.kts
android {
    buildTypes {
        release {
            matchingFallbacks += listOf("release", "debug")
        }
    }
}

Or fix in the shared module — explicitly declare build types:

// shared/build.gradle.kts
android {
    buildTypes {
        release { }
        debug { }
    }
}

Step 5 — Build Release AAB

# Module name determines the command — use the correct one:
./gradlew :composeApp:bundleRelease
# or
./gradlew :androidApp:bundleRelease

Output path — filename matches the module name, NOT app-release.aab:

  • composeApp/build/outputs/bundle/release/composeApp-release.aab
  • androidApp/build/outputs/bundle/release/androidApp-release.aab

Step 6 — Verify Before Upload

# Adjust module name in paths below (composeApp or androidApp)

# Verify signing — confirm alias is "upload", NOT "androiddebugkey"
keytool -printcert -jarfile composeApp/build/outputs/bundle/release/composeApp-release.aab

# Verify version (requires bundletool)
bundletool dump manifest --bundle=composeApp/build/outputs/bundle/release/composeApp-release.aab \
  | grep -E "versionCode|versionName"

Checklist:

  • AAB signed with upload key (not debug)
  • versionCode higher than the previous upload
  • keystore.properties and *.jks in .gitignore
  • isMinifyEnabled = true and isShrinkResources = true both set
  • Shared module consumerProguardFiles configured (if shared uses reflection-heavy libs)
  • Variant alignment verified (shared module has release build type or matchingFallbacks set)

NEVER Do

  1. NEVER assume output is app-release.aab — KMP output filename matches the module name: composeApp-release.aab or androidApp-release.aab. CI scripts, upload commands, and Fastlane configs that hardcode app-release.aab will silently fail to find the file or upload nothing.

  2. NEVER use Groovy DSL in a KMP project — The KMP Gradle plugin only supports .gradle.kts. A .gradle file in a KMP module silently breaks multiplatform dependency resolution. Do not convert to Groovy, do not mix DSLs.

  3. NEVER put ProGuard rules only in the Android module — Rules in composeApp/proguard-rules.pro only apply to that module’s direct code. If shared/ uses Ktor or kotlinx.serialization, the shared module MUST publish its own rules via consumerProguardFiles("consumer-rules.pro"). Otherwise R8 strips shared module classes and the app crashes at runtime with no build-time warning.

  4. NEVER ignore variant mismatch errors — “Could not resolve :shared variant” is NOT a generic Gradle issue. It means the shared module’s build types don’t align with the Android module. Fix with matchingFallbacks, not by deleting the dependency.

  5. NEVER set different store/key passwords with PKCS12 — keytool silently uses store password for key. Different passwords → signing fails with “Cannot recover key” (misleading — it’s a password mismatch).

  6. NEVER skip testing the signed AAB on a real device — R8 stripping in multi-module KMP is invisible until runtime. Ktor engine stripped → network calls crash. Serialization stripped → data parsing crashes. These only manifest in release builds.


Common Errors

Error Cause Fix
ClassNotFoundException: io.ktor.* at runtime R8 stripped Ktor engine (ServiceLoader) Add Ktor -keep rules in shared/consumer-rules.pro
kotlinx.serialization crash at runtime @Serializable serializers stripped by R8 Add serialization rules in shared consumer-rules
Could not resolve :shared variant Shared module missing release build type Add matchingFallbacks in consuming module
kotlinx.datetime missing at runtime R8 removed JVM datetime implementation Add -keep class kotlinx.datetime.**
AAB not found by CI/upload script Script looks for app-release.aab Use <module>-release.aab (module name as prefix)
Missing class: ... during R8 R8 strips classes used via reflection Add -keep rules from build error output

Gotchas

  1. AGP + Kotlin version matrix — KMP requires compatible AGP and Kotlin plugin versions. Mismatches produce cryptic “Cannot find plugin” or “Unsupported metadata version” errors. Always check libs.versions.toml for version alignment. AGP 9/10 introduce breaking changes in the library plugin API.

  2. Multiple shared modules — If the project has core/, data/, domain/ as separate KMP modules, EACH module that uses reflection-heavy libraries needs its own consumerProguardFiles. One missing module = one runtime crash.

  3. App Signing by Google Play — Google re-signs your app with their app signing key. The keystore you generate is the upload key only. If you lose it, request a reset through Play Console (takes days, requires identity verification).