Skip to content

Setup guides · Kotlin / Jetpack Compose

Google Play closed testing for Kotlin and Jetpack Compose apps

For native Android projects in Android Studio: build a signed release bundle from the IDE or with Gradle, set up the closed track and keep 12 testers opted in for 14 days, without the release-only bugs that R8 and new API levels like to introduce.

Build
./gradlew bundleRelease
Version code
defaultConfig → versionCode
Target API
targetSdk = 36

Build a signed release bundle

  1. Set the application ID

    In app/build.gradle.kts, applicationId inside defaultConfig identifies the app on Google Play and can't change after the first upload. namespace is only for code and can differ.

  2. 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.

  3. 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/.

Terminal
./gradlew bundleRelease

Before 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:

Terminal
./gradlew installRelease

Upload 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:

keystore.properties
storePassword=myStorePassword
keyPassword=myKeyPassword
keyAlias=upload
storeFile=/path/to/upload-keystore.jks
app/build.gradle.kts
import 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:

app/build.gradle.kts
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 to proguard-rules.pro and test the release variant.
  • Edge-to-edge surprises. Content drawn under the status and navigation bars hides buttons. Use Scaffold padding or WindowInsets modifiers on every screen testers will visit.
  • Debuggable release. isDebuggable = true in the release build type makes Play Console refuse the bundle.
  • Signing secrets in git. Inline passwords in build.gradle.kts end up in your repository. Keep them in keystore.properties or CI secrets.
  • Native libraries and 16 KB pages. Pure Kotlin apps already support 16 KB memory pages, but NDK code and libraries that ship .so files 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.

  1. 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.

  2. Keep a copy per release

    Archive mapping.txt with every version code you upload. You need it for traces testers paste into bug reports and for any crash tool outside Google Play.

  3. Retrace by hand

    Run retrace from the command-line tools with the mapping and a file containing the trace. Recent Android Studio versions also retrace stack traces in Logcat automatically.

Terminal (project root)
$ANDROID_HOME/cmdline-tools/latest/bin/retrace \
  app/build/outputs/mapping/release/mapping.txt trace.txt

Using 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.

app/build.gradle.kts
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 .aab and 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.

Open the Play Console steps

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.

Terminal
./gradlew bundleRelease

To 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:

Terminal
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.apks

For 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.

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

Need 12 real testers for 14 days?

Add your app and closed testing link, pick Starter (15 testers) or Pro (25 testers), and follow every tester's activity day by day in your panel.

TestBunny isn't affiliated with Google. Testers earn credits, never cash. Nobody can guarantee Google's production access decision, and we don't.