Build a signed release bundle
Set the application ID
In
app/build.gradle.kts,applicationIdinsidedefaultConfigidentifies the app on Google Play and can't change after the first upload.namespaceis only for code and can differ.Build from Android Studio
Choose Build → Generate Signed Bundle/APK, select Android App Bundle, pick your keystore and key (or Create new), choose the release build variant and click Create.
Or build from the command line
Once signing is configured in Gradle (next section), run the task below. The bundle is written to
app/build/outputs/bundle/release/.
./gradlew bundleReleaseBefore you upload, install the release variant on a device. Bugs caused by R8 or by release-only configuration never show up in a debug run:
./gradlew installReleaseUpload key and Play App Signing
New apps must use Play App Signing: Google keeps the app signing key, and you sign each upload with an upload key. The Android Studio wizard can create the keystore; it recommends a validity of at least 25 years.
To sign from the command line or CI, keep the credentials in a keystore.properties file outside version control and read it in app/build.gradle.kts:
storePassword=myStorePassword
keyPassword=myKeyPassword
keyAlias=upload
storeFile=/path/to/upload-keystore.jksimport java.io.FileInputStream
import java.util.Properties
val keystoreProperties = Properties().apply {
load(FileInputStream(rootProject.file("keystore.properties")))
}
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
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("release")
}
}
}Back up the keystore and both passwords. If the upload key is lost or leaked, create a new one and request an upload key reset in Play Console; the app signing key stays the same.
Version code and target API level
versionCode must be higher than every bundle you've uploaded, on any track; versionName is what users see. Both live in defaultConfig, next to the SDK levels:
android {
compileSdk = 36
defaultConfig {
applicationId = "com.yourcompany.app"
targetSdk = 36
versionCode = 2
versionName = "1.0.1"
}
}Since 31 August 2026, new apps and updates must target Android 16 (API level 36). Raising targetSdk turns on new platform behavior. Apps targeting Android 15 and higher are drawn edge-to-edge, and on Android 16 the opt-out no longer works, so check that your Compose screens handle window insets before the test starts.
Kotlin and Compose pitfalls
- R8 removes what reflection needs. With
isMinifyEnabled = true, serialization models, Retrofit or Gson classes and some dependency injection setups can break only in release. Add keep rules toproguard-rules.proand test the release variant. - Edge-to-edge surprises. Content drawn under the status and navigation bars hides buttons. Use
Scaffoldpadding orWindowInsetsmodifiers on every screen testers will visit. - Debuggable release.
isDebuggable = truein the release build type makes Play Console refuse the bundle. - Signing secrets in git. Inline passwords in
build.gradle.ktsend up in your repository. Keep them inkeystore.propertiesor CI secrets. - Native libraries and 16 KB pages. Pure Kotlin apps already support 16 KB memory pages, but NDK code and libraries that ship
.sofiles must be rebuilt or updated. Play Console warns about it, and from 1 February 2027 updates without support can't be released. - Only testing debug. Testers get the release variant. Run it on at least one recent Android version before every upload.
Readable stack traces: R8 mapping and native symbols
Testers only ever run the release variant, so their crashes come from R8-optimized code with renamed classes and methods. The build writes the mapping that undoes this to app/build/outputs/mapping/release/mapping.txt, overwrites it on every build and also bundles it into the .aab.
Let Play Console deobfuscate
With Android Gradle plugin 4.1 or later, Play Console takes the mapping file from the uploaded bundle. Crashes under Monitor and improve → Android vitals → Crashes & ANRs then show your real class and method names.
Keep a copy per release
Archive
mapping.txtwith every version code you upload. You need it for traces testers paste into bug reports and for any crash tool outside Google Play.Retrace by hand
Run
retracefrom the command-line tools with the mapping and a file containing the trace. Recent Android Studio versions also retrace stack traces in Logcat automatically.
$ANDROID_HOME/cmdline-tools/latest/bin/retrace \
app/build/outputs/mapping/release/mapping.txt trace.txtUsing the NDK? Set debugSymbolLevel on the release build type. SYMBOL_TABLE gives function names in Play Console, FULL adds files and line numbers. For app bundles, the native symbols are included and uploaded automatically.
android {
buildTypes {
release {
ndk {
debugSymbolLevel = "FULL" // or "SYMBOL_TABLE"
}
}
}
}When R8 keeps code you didn't expect, or removes code reflection needs, -whyareyoukeeping shows the path from a kept class to an entry point, and configuration.txt next to the mapping lists every merged rule.
Play Console in short: from closed test to production
Once your Kotlin / Jetpack Compose bundle is built and signed, the rest happens in Play Console and works the same for every framework. This is the checklist; the full Play Console walkthrough covers every menu and screen.
- Finish app setup on the Dashboard: privacy policy, app access, ads, content rating, target audience, data safety and store listing.
- Create a closed testing release under Test and release → Testing → Closed testing: choose your testers' countries, upload the
.aaband send it for review. - Add testers with a Google Group on the Testers tab and share the opt-in link. It works once the track shows Published.
- Keep at least 12 testers opted in for 14 days in a row. Internal testing doesn't count, and someone who leaves and rejoins starts their 14 days again.
- Ship at least one update during the test and write down what testers found and what you changed.
- Apply for production from the Dashboard and answer the three sections about your test, your app and your readiness. The decision is Google's; nobody can guarantee approval.
Plan for drop-outs: recruit more than 12 testers so one person leaving doesn't push back your timeline. See the full Play Console steps.
The full Play Console walkthrough
Closed track, Google Group and opt-in link, the 12 testers / 14 days rule and the production application, step by step on one page.
Test the exact bundle and ship updates
Raise versionCode, build the bundle again (Gradle or the Android Studio wizard) and upload it to the same closed track.
./gradlew bundleReleaseTo install the bundle you're about to upload, generate device-specific APKs from it with bundletool, signed with your upload key. That's the closest you get to what Google Play delivers, including the per-device splits for screen density, language and ABI:
bundletool build-apks --connected-device \
--bundle=app/build/outputs/bundle/release/app-release.aab --output=app.apks \
--ks=/path/to/upload-keystore.jks --ks-key-alias=upload
bundletool install-apks --apks=app.apksFor quick checks by your own team, Play Console's internal app sharing accepts any version code and gives you a download link for up to 100 people. Those uploads can't be part of a testing track and don't replace the closed test: your 12 testers always install from the closed track.
Frequently asked questions
Do I have to upload mapping.txt myself?
Not for app bundles built with Android Gradle plugin 4.1 or later: Play Console takes it from the .aab. APKs need a manual upload, and crash tools outside Google Play need their own copy, so archive the file either way.
Is minification required?
No. isMinifyEnabled = true is optional: it shrinks and obfuscates your code, and it's also the most common reason a release build crashes while debug works. If you turn it on, test the release variant before every upload.
Do Kotlin apps need work for 16 KB memory pages?
Apps written only in Kotlin or Java already support 16 KB pages. With NDK code or libraries that ship .so files, use Android Gradle plugin 8.5.1 or higher and NDK r28 or higher, update those libraries and test on a 16 KB emulator image.
Should I run internal testing first?
You can. Internal testing is quick to set up and good for a smoke test with your team, but it doesn't count toward the 12 testers rule, so don't let it delay the closed test.
Sources
Technical details were checked against the official documentation below. Menus and defaults change between versions; when in doubt, the official page wins.
- Android Developers: Sign your app
- Android Developers: Target API level requirements
- Android Developers: Support 16 KB page sizes
- Android Developers: Shrink, obfuscate, and optimize your app
- Android Developers: Troubleshoot app optimization (mapping.txt, retrace)
- Android Developers: Include native debug symbols
- Android Developers: bundletool
- Play Console Help: Internal app sharing
- Play Console Help: App testing requirements for new personal developer accounts
- Play Console Help: Set up an open, closed, or internal test
- Play Console Help: Deobfuscate or symbolicate crash stack traces
New to the 12 testers rule?
The general guide explains Google's requirement, how a TestBunny campaign runs day by day and what we can and can't guarantee.
Read the general guide