playstore-kmp
npx skills add https://github.com/tacuchi/playstore-kmp --skill playstore-kmp
Agent 安装分布
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:
-
Which is the Android module? Look for the directory containing an
android {}block withapplicationId:composeApp/â Compose Multiplatform template (KMP Wizard default)androidApp/â Classic KMP template- Custom name â Read
settings.gradle.ktsforinclude(":moduleName") - This module name determines: build command, output path, and AAB filename
-
Shared module structure? Check for modules that the Android module depends on:
- Has
android {}withlibraryNamespaceâ It’s a KMP library module; needsconsumerProguardFiles - 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
- Has
-
Variant alignment? Check if shared/library modules define
releasebuild type:- Has
releasebuild 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
- Has
-
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)
-
Existing keystore? Ask the user before generating a new one
- Already has
.jksâ Reuse it, skip Step 1 - First release â Generate new keystore
- Already has
-
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.keytoolsilently uses the store password for the key. Different passwords â signing fails later with misleading “Cannot recover key” error.- Store the
.jksoutside 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/orandroidApp/), NOT in rootbuild.gradle.kts. KMP root build file typically only applies plugins. rootProject.file()scope:keystore.propertiesis in project root. From withincomposeApp/build.gradle.kts,rootProject.file()correctly resolves to root.project.file()would look insidecomposeApp/.- 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.aabandroidApp/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)
-
versionCodehigher than the previous upload -
keystore.propertiesand*.jksin.gitignore -
isMinifyEnabled = trueandisShrinkResources = trueboth set - Shared module
consumerProguardFilesconfigured (if shared uses reflection-heavy libs) - Variant alignment verified (shared module has release build type or matchingFallbacks set)
NEVER Do
-
NEVER assume output is
app-release.aabâ KMP output filename matches the module name:composeApp-release.aaborandroidApp-release.aab. CI scripts, upload commands, and Fastlane configs that hardcodeapp-release.aabwill silently fail to find the file or upload nothing. -
NEVER use Groovy DSL in a KMP project â The KMP Gradle plugin only supports
.gradle.kts. A.gradlefile in a KMP module silently breaks multiplatform dependency resolution. Do not convert to Groovy, do not mix DSLs. -
NEVER put ProGuard rules only in the Android module â Rules in
composeApp/proguard-rules.proonly apply to that module’s direct code. Ifshared/uses Ktor or kotlinx.serialization, the shared module MUST publish its own rules viaconsumerProguardFiles("consumer-rules.pro"). Otherwise R8 strips shared module classes and the app crashes at runtime with no build-time warning. -
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. -
NEVER set different store/key passwords with PKCS12 â
keytoolsilently uses store password for key. Different passwords â signing fails with “Cannot recover key” (misleading â it’s a password mismatch). -
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
-
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.tomlfor version alignment. AGP 9/10 introduce breaking changes in the library plugin API. -
Multiple shared modules â If the project has
core/,data/,domain/as separate KMP modules, EACH module that uses reflection-heavy libraries needs its ownconsumerProguardFiles. One missing module = one runtime crash. -
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).