An app that runs on your emulator is not yet an app that people can install. Between "it works on my laptop" and "it is on Google Play" there is a set of steps: choose a permanent identity, set version numbers, shrink and protect your code, sign the app with a key only you hold, build an Android App Bundle, and test it with real users before the world sees it. Mistakes here are expensive. A lost signing key can mean you can never update your app, and a debug build uploaded by accident can leak secrets.
This lesson is the foundation for the Google Play Store Listing Certificate modules in lessons 50–53 and covers the Preparing and publishing an app material from the Android developer documentation (https://developer.android.com/studio/publish). It also follows the final steps of the Android Basics with Compose programme.
By the end you will be able to:
- Explain the difference between debug and release builds.
- Set and understand
applicationId,versionCodeandversionName. - Create an upload key, store it safely, and configure Gradle to sign a release build without putting passwords in source control.
- Explain Play App Signing and why Google holds the app signing key.
- Build an Android App Bundle (AAB) and explain how it differs from an APK.
- Turn on R8 (shrinking, obfuscation and optimisation) and write keep rules.
- Meet Google Play's target API level requirement.
- Test with internal, closed, open tracks and read the pre-launch report.
- Run through a release checklist.
Big idea: a release is a process, not a button. Make it repeatable, write it down, and never depend on one person's memory or one person's laptop.
- Gradle files, Kotlin DSL and version catalogs (lessons 3 and 5).
- Build variants
debugandrelease(lesson 5). - Manifest permissions (lesson 29).
- Running on a physical device (lesson 5).
- Android Studio tools (lesson 44).
source code
|
| Gradle build (release variant)
v
compile -> R8 shrink/obfuscate/optimise -> package -> SIGN (upload key)
|
v
app bundle (.aab) ---- upload ----> Google Play Console
|
Play re-signs with the app signing key
|
generates optimised APKs per device
|
v
user's phone installs
| Term | Meaning |
|---|---|
| APK | Android Package: the installable file. One APK contains everything for all devices |
| AAB (Android App Bundle) | The publishing format you upload to Play. Google builds device-specific APKs from it, so downloads are smaller |
| Upload key | The key you keep and use to sign the file you upload |
| App signing key | The key Google Play uses to sign APKs delivered to users (managed by Play App Signing) |
| Keystore | A file that holds one or more keys, protected by passwords |
| Alias | The name of a key inside a keystore |
| Track | A release channel in Play Console: internal, closed, open, production |
applicationId: your app's permanent identity
Open app/build.gradle.kts:
android {
namespace = "com.techbase.classregister"
defaultConfig {
applicationId = "com.techbase.classregister"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0.0"
}
}
(Use the targetSdk and minSdk that Android Studio's current template chooses and that Google Play requires: see the target API section below. The numbers above are an example.)
namespaceis the package used for the generatedRclass and your Kotlin code organisation. It is not what users see.applicationIdis the unique identifier of your app on the device and on Google Play. It must be unique across all of Google Play, uses lowercase letters, digits, underscores and dots, has at least two segments, and can never be changed after you publish, because a different ID means a different app. Choose it carefully, typically a reversed domain you control:com.techbase.classregister. Do not usecom.example, which Google Play rejects.- If the namespace and applicationId differ, that is fine and common.
versionCode and versionName
| Field | Type | Who sees it | Rules |
|---|---|---|---|
versionCode |
Integer | Android and Play (not users) | Must increase with every upload. 1, 2, 3... A higher number means newer. Maximum 2,100,000,000 |
versionName |
String | Users | Free text such as "1.0.0" or "2.3 beta" |
Google Play rejects an upload whose versionCode is not higher than all previous uploads of that app on any track. A tidy approach is to derive the version from a single place, for example gradle.properties:
APP_VERSION_CODE=7
APP_VERSION_NAME=1.2.0
android {
defaultConfig {
versionCode = (project.property("APP_VERSION_CODE") as String).toInt()
versionName = project.property("APP_VERSION_NAME") as String
}
}
Use semantic versioning for versionName: MAJOR.MINOR.PATCH. Increase MAJOR for big or breaking changes, MINOR for new features, PATCH for fixes. Bigger teams generate versionCode from the CI build number so that it increases automatically.
The app name and icon
Set android:label="@string/app_name" in the manifest, and make a proper adaptive icon (lesson 21, Image Asset Studio). Check the name looks right on a home screen where it is truncated to roughly 12 characters.
Gradle has two default build types. You can add more (for example staging).
android {
buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
isDebuggable = true
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// signingConfig is added in the signing section below
}
}
}
| Setting | Debug | Release |
|---|---|---|
| Debuggable | Yes | No |
| Code shrinking / obfuscation (R8) | Off | On |
| Signed with | An automatic debug key | Your upload key |
| Logging | Verbose | Minimal; remove sensitive logs |
| Server URLs / API keys | Test | Production |
| Performance | Slower | Optimised |
applicationIdSuffix = ".debug" lets a debug and a release build be installed on the same phone at the same time because they have different IDs. Use BuildConfig fields (enable with buildFeatures { buildConfig = true }) and per-build-type resources to switch environments, not if (BuildConfig.DEBUG) scattered through code.
Never ship a debuggable build. Play rejects an uploaded bundle with
debuggable=true, and it is a security risk.
Why apps are signed
Android requires every installed app to be digitally signed. The signature proves two things: the update came from the same publisher as the original, and the file was not changed after signing. Lose the key, or let someone steal it, and you lose control of your app.
Play App Signing: the modern model
With Play App Signing (required for new apps on Google Play):
- Google holds the app signing key in its secure infrastructure and signs the APKs delivered to users.
- You create an upload key and use it to sign your AAB before upload. Google verifies the upload signature, strips it and re-signs with the app signing key.
- If you lose your upload key, you can request a reset from Google, because the app signing key (which users' phones trust) is safe. In the old model, losing the only key meant you could never update the app again.
Even so: guard the upload key carefully, because anyone with it could upload an update to your app (if they also have Console access).
Create an upload keystore in Android Studio
- Build → Generate Signed App Bundle / APK…
- Choose Android App Bundle, then Next.
- Under Key store path click Create new….
- Choose a location outside your project folder (for example
~/keys/techbase-upload.jks), set a strong keystore password, an alias (for exampleupload), a key password, a validity of at least 25 years (Google requires the key to be valid beyond October 2033; 25+ years is the usual guidance) and fill the certificate fields (name, organisation, country codeNG). - Click OK, then continue the wizard.
Or from the command line (the keytool that ships with the JDK):
keytool -genkeypair -v \
-keystore ~/keys/techbase-upload.jks \
-alias upload \
-keyalg RSA -keysize 2048 \
-validity 10000
-validity 10000 is in days (about 27 years). The tool asks for passwords and details.
Back up the keystore and write the passwords in a password manager, in at least two secure places (for example an encrypted drive and your organisation's password manager). Never commit the keystore or its passwords to Git. Add
*.jks,*.keystoreandkeystore.propertiesto.gitignore.
Configure Gradle to sign without exposing secrets
Create a file keystore.properties in the project root (and add it to .gitignore):
storeFile=/home/you/keys/techbase-upload.jks
storePassword=YOUR_STORE_PASSWORD
keyAlias=upload
keyPassword=YOUR_KEY_PASSWORD
Then read it in app/build.gradle.kts:
import java.io.FileInputStream
import java.util.Properties
val keystoreProperties = Properties().apply {
val file = rootProject.file("keystore.properties")
if (file.exists()) load(FileInputStream(file))
}
android {
signingConfigs {
create("release") {
if (keystoreProperties.isNotEmpty()) {
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
}
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
The if (keystoreProperties.isNotEmpty()) guard lets teammates and CI jobs without the key still sync the project (they just cannot produce a signed release). In CI, supply the same values through encrypted secrets and write the file at build time, or read environment variables (System.getenv("STORE_PASSWORD")).
Build the bundle
./gradlew bundleRelease
The output is app/build/outputs/bundle/release/app-release.aab. For a quick on-device APK test of the release variant:
./gradlew assembleRelease
producing app/build/outputs/apk/release/app-release.apk (signed with your release config). In Android Studio use the Build Variants panel to switch the active variant to release and run it on a device, which is the correct way to test what users will really get.
Why AAB?
An APK must contain resources for every screen density, every CPU architecture and every language. An App Bundle contains all of them separately, and Play assembles a split APK set just for each user's phone. Typical results: noticeably smaller downloads and installs, which matters a great deal for users on limited data plans or low-storage phones. AAB is required for new apps on Google Play.
You can test how Play will split your bundle locally with bundletool (a command-line tool from Google):
# Build a set of APKs for a connected device from the bundle
java -jar bundletool.jar build-apks \
--bundle=app/build/outputs/bundle/release/app-release.aab \
--output=app.apks \
--ks=/home/you/keys/techbase-upload.jks --ks-key-alias=upload
# Install exactly what that device would receive
java -jar bundletool.jar install-apks --apks=app.apks
(Download bundletool from its official GitHub releases page; the exact command options are documented there.)
Inspect the final APK
Build → Analyze APK… opens a release APK or AAB and shows what takes space: classes.dex, resources, native libraries, assets. Large images and unused libraries are the common culprits. Consider WebP images, vector drawables and removing unused dependencies.
isMinifyEnabled = true turns on R8, Android's code shrinker. It does three things:
| Feature | What it does |
|---|---|
| Shrinking | Removes classes and methods your app never uses |
| Obfuscation | Renames classes and members to short names (a, b) to make reverse engineering harder and code smaller |
| Optimisation | Rewrites code for speed (inlining and more) |
isShrinkResources = true |
Also removes unused resources (layouts, images, strings) |
Because R8 analyses code statically, it can remove or rename things your app uses indirectly (via reflection, JSON field names, native code). The usual symptoms: the release app crashes with ClassNotFoundException or NoSuchMethodException, or JSON parsing silently returns empty fields, while the debug app works. The cure is a keep rule in proguard-rules.pro:
# Keep the data classes used with Gson/Moshi reflection
-keep class com.techbase.classregister.data.remote.dto.** { *; }
# Keep enum names used in serialisation
-keepclassmembers enum * { *; }
# Keep line numbers in stack traces (helps Play crash reports)
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFileGood news: modern libraries (Room, Retrofit with kotlinx.serialization, Hilt, WorkManager, Compose) ship their own consumer rules and need little or none from you. Most keep rules you will write are for your own classes accessed by reflection. Always read the library's documentation for required rules and test the release build thoroughly.
Mapping file
R8 produces a mapping file (app/build/outputs/mapping/release/mapping.txt) that translates obfuscated stack traces back to readable names. When you upload a bundle built with R8, Android Studio's bundle and Play Console accept the mapping automatically in modern setups (it is included in the bundle's metadata); keep a copy of the mapping file for each released version anyway, since you cannot reconstruct it.
Google Play has rules that change, so always read the current Help pages.
Target API level
At the time of writing (October 2026), the official page states that from August 31, 2026, new apps and app updates must target Android 16 (API level 36) or higher (with lower requirements for Wear OS, Automotive, TV and XR), and existing apps must target at least Android 15 (API 35) to stay available to new users on newer devices. A one-time extension to November 1, 2026 can be requested in Play Console. Check the current page before every release: https://developer.android.com/google/play/requirements/target-sdk.
In Gradle, targetSdk tells Android which behaviour changes your app has been tested against; compileSdk is the SDK you compile with (should be at least targetSdk); minSdk is the oldest Android version you support. Raising targetSdk opts you into new behaviour (for example edge-to-edge, notification and background restrictions), so read the behaviour changes for each API level and test.
Other items to check before publishing
- Permissions: request only what you need; explain why (lesson 29 for
INTERNET). Sensitive permissions (SMS, call logs, background location) need a Play Console declaration and may be refused. - Data safety form and privacy policy: describe what data you collect and share. If your app handles personal data you need a privacy policy URL.
- Content rating questionnaire: completed in Play Console.
- Ads, in-app purchases, subscriptions: policies apply.
- Family and children's apps: stricter rules.
- App access: if reviewers need a login, provide test credentials.
- 64-bit and 16 KB page-size requirements: native libraries must meet current requirements. If your app has no native code (pure Kotlin/Compose) this is automatic; check the documentation if you include native libraries.
- Developer verification: Google is introducing verification requirements for developers; check the current Play and Android developer-verification announcements.
Never go straight from your laptop to everyone. Play Console offers tracks:
| Track | Audience | Use it for |
|---|---|---|
| Internal testing | Up to 100 testers you list by email | Quick checks of every build; available within minutes |
| Closed testing | Invited testers (email lists or Google Groups) | Beta with friends, classmates, a client |
| Open testing | Anyone can join from the store listing | Public beta |
| Production | Everyone, optionally with staged rollout (for example 10% of users) | The real release |
New personal developer accounts (created after November 13, 2023) must run a closed test with at least 12 testers opted in continuously for 14 days before they can apply for production access, according to Play Console Help at the time of writing. Organisation accounts are treated differently. Check the current rule in Help before planning your launch date.
The pre-launch report
When you upload to a testing track, Google can automatically install your app on a set of real devices and crawl it, then produce a pre-launch report with crashes, performance, accessibility checks (the Accessibility Scanner-style findings of lesson 48), security vulnerabilities and screenshots of devices. Read it before you promote a release.
Staged rollout and monitoring
In production choose a staged rollout percentage, watch Android vitals (crash rate, ANR rate) and reviews, then increase. If something is wrong, halt the rollout. You cannot "un-publish" an update for users who already have it, so you need to release a fix with a higher versionCode.
Copy this into your repository as RELEASE.md:
BEFORE BUILD
[ ] applicationId final and unique (not com.example)
[ ] versionCode increased; versionName updated; changelog written
[ ] targetSdk meets the current Play requirement; tested on that API
[ ] minSdk decision documented
[ ] All debug code, test endpoints and logs with sensitive data removed
[ ] API keys not hard-coded in source (use secrets plugin or server-side keys)
[ ] Permissions reviewed; unused ones removed
[ ] Strings translated or reviewed; no placeholder text
BUILD
[ ] Release variant selected; isMinifyEnabled and isShrinkResources true
[ ] keystore.properties present; keystore NOT in version control
[ ] ./gradlew clean bundleRelease succeeds
[ ] Release APK tested on at least 2 real devices (one low-end)
[ ] Core flows tested after R8 (login, save, network, rotate, back)
[ ] mapping.txt archived with the version
PLAY CONSOLE
[ ] Upload AAB to internal track first
[ ] Data safety, content rating, privacy policy and app access complete
[ ] Pre-launch report reviewed
[ ] Store listing complete (lessons 50-53)
[ ] Staged rollout configured
AFTER
[ ] Tag the commit: git tag v1.2.0
[ ] Watch Android vitals and reviews for 48 hours
Exercise 1: Make a release build
Take any project from this course (for example the Class Register or Naija Snacks). Choose a real applicationId, set version 1/1.0.0, create an upload keystore outside the project, add keystore.properties to .gitignore, configure the signing, and produce a signed app-release.aab. List the commands and files involved.
Steps and artefacts:
applicationId = "com.yourname.classregister",versionCode = 1,versionName = "1.0.0"indefaultConfig.keytool -genkeypair -v -keystore ~/keys/upload.jks -alias upload -keyalg RSA -keysize 2048 -validity 10000.- Create
keystore.propertiesat the root withstoreFile,storePassword,keyAliasandkeyPassword; add these lines to.gitignore:
keystore.properties
*.jks
*.keystore
- Add the
signingConfigs.create("release") { ... }block reading the properties and setsigningConfig = signingConfigs.getByName("release")in thereleasebuild type. ./gradlew clean bundleRelease.- Find
app/build/outputs/bundle/release/app-release.aab. - Check with
git statusthat no key file or properties file appears as untracked.
Exercise 2: Fix a release-only crash
Your debug app parses JSON into a data class Student(val name: String, val score: Int) using a reflection-based library. The release build shows empty names. Explain why, and give two fixes.
R8 renamed the fields of Student (name became a), so reflection-based JSON mapping no longer finds the JSON keys. Fix 1: add a keep rule for the data classes, such as -keep class com.yourname.app.data.remote.dto.** { *; }. Fix 2: use a serialisation approach that does not rely on field names at runtime: kotlinx.serialization with @Serializable generates code at compile time (and annotate names with @SerialName), so R8 can safely rename the fields. Then re-test the release build, not only the debug build.
Exercise 3: Version numbers
Your last upload was versionCode = 12, versionName = "1.3.0". You fix a crash. What should the new values be, and what happens if you upload versionCode = 12 again?
New values: versionCode = 13 (any higher integer) and versionName = "1.3.1" (a patch release). Uploading versionCode = 12 again is rejected by Play Console with an error saying the version code has already been used (or is not higher than the existing one); versionCode must increase on every upload to any track.
Mistake #1: Keeping com.example as the application ID. Symptom: Play rejects the upload. Fix: choose a real, unique ID before the first release.
Mistake #2: Committing the keystore or passwords. Symptom: anyone with repository access can sign as you. Fix: .gitignore, secrets in CI, rotate if leaked (upload key reset through Play Console).
Mistake #3: Losing the only copy of the keystore. Fix: back up; Play App Signing allows an upload-key reset, but it takes time.
Mistake #4: Forgetting to raise versionCode. Symptom: upload rejected. Fix: automate it.
Mistake #5: Only testing the debug build. Symptom: release-only R8 crashes. Fix: test the release variant on real devices.
Mistake #6: Uploading an APK instead of an AAB. Symptom: not accepted for new apps. Fix: bundleRelease.
Mistake #7: Hard-coding API keys in source. Symptom: extractable from the APK. Fix: keep real secrets on a server; for client-side keys, use restrictions (package name and signing certificate) in the provider's console.
Mistake #8: Ignoring the target API deadline. Symptom: cannot publish updates. Fix: track the requirement and test on the new API early.
Mistake #9: Shipping with logging of personal data. Fix: strip or avoid sensitive logs.
Mistake #10: Releasing to 100% immediately. Fix: internal, closed, then staged production rollout.
Question 1. What is the difference between versionCode and versionName?
versionCode is an integer used by Android and Play to order versions; it must increase with each upload. versionName is a human-readable string shown to users, such as "1.3.1".
Question 2. What does Play App Signing do?
Google stores and uses the app signing key to sign the APKs delivered to users, while you sign uploads with a separate upload key that can be reset if lost.
Question 3. Why does the release build sometimes crash when the debug build does not?
R8 shrinks and obfuscates code in release builds and can remove or rename classes and fields used through reflection. Missing keep rules cause crashes or missing data. Debug builds do not run R8.
Question 4. What is an Android App Bundle and why is it used?
It is the publishing format that contains all code and resources; Google Play builds optimised, smaller APKs for each user's device from it, so downloads are smaller.
Question 5. Why use testing tracks before production?
They let you check a build with limited audiences and automated tests (the pre-launch report) and fix problems before everyone sees it, and a staged production rollout limits the damage of any remaining bug.
Go deeper: official resources
- Prepare your app for release (Android Studio docs)
- Target API level requirements
- Google Play Console Help
- Android Basics with Compose course
Project goal: Take the Class Register (or any app you built) from debug-only to a repeatable, signed, shrunk release, with documentation another developer could follow.
Requirements:
- A real
applicationId,versionCode/versionNamedriven fromgradle.properties. - An upload keystore outside the repo,
keystore.propertiesignored by Git, and a signing config that tolerates its absence. releasewith R8 and resource shrinking, and aproguard-rules.prowith at least the line-number rules.- A
debugbuild type with anapplicationIdSuffixso both builds can be installed together. ./gradlew bundleReleaseproduces a signed AAB; a release APK built withassembleReleaseis installed and the core flows tested.- A
RELEASE.mdchecklist in the repo. - Record in a short note: AAB size, APK size and what Analyze APK showed as the biggest item.
Stretch goal: write a GitHub Actions workflow that builds the bundle in CI using encrypted secrets for the keystore (base64-encoded and decoded at build time).
gradle.properties additions:
APP_VERSION_CODE=1
APP_VERSION_NAME=1.0.0
app/build.gradle.kts (key parts):
import java.io.FileInputStream
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
val keystoreProperties = Properties().apply {
val f = rootProject.file("keystore.properties")
if (f.exists()) load(FileInputStream(f))
}
android {
namespace = "com.yourname.classregister"
compileSdk = 36
defaultConfig {
applicationId = "com.yourname.classregister"
minSdk = 24
targetSdk = 36
versionCode = (project.property("APP_VERSION_CODE") as String).toInt()
versionName = project.property("APP_VERSION_NAME") as String
}
signingConfigs {
create("release") {
if (keystoreProperties.isNotEmpty()) {
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
}
}
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
release {
isMinifyEnabled = true
isShrinkResources = true
signingConfig = signingConfigs.getByName("release")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
.gitignore lines:
keystore.properties
*.jks
*.keystore
local.properties
proguard-rules.pro:
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFileCommands:
./gradlew clean bundleRelease
./gradlew assembleRelease
adb install -r app/build/outputs/apk/release/app-release.apk
Optional CI workflow (.github/workflows/release.yml):
name: Release bundle
on:
push:
tags: ["v*"]
jobs:
bundle:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: Restore keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/upload.jks"
cat > keystore.properties <<EOF
storeFile=$RUNNER_TEMP/upload.jks
storePassword=${{ secrets.KEYSTORE_PASSWORD }}
keyAlias=${{ secrets.KEY_ALIAS }}
keyPassword=${{ secrets.KEY_PASSWORD }}
EOF
- run: ./gradlew bundleRelease
- uses: actions/upload-artifact@v4
with:
name: aab
path: app/build/outputs/bundle/release/*.aab
Check the current versions of the actions and the Java version your Android Gradle Plugin requires. Expected result: the signed .aab appears in app/build/outputs/bundle/release/, the release APK installs next to the debug build (different IDs), and nothing secret is tracked by Git. Your note should record the two sizes and the largest item from Analyze APK (commonly classes.dex or images).
Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.
Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 49 project”.
On github.com click New repository, name it android-lesson-49-prepare-your-app-for-release, set it to Public, leave Add a README unticked, and click Create repository.
Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.
Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉
Lesson 49 complete! 🎉
You covered:
- Lesson Introduction
- Prerequisite Concepts
- Conceptual Understanding: The Journey from Code to Store
- Step-by-Step Build 1: Application ID and Version Numbers
- Step-by-Step Build 2: Build Types and What Release Changes
- Step-by-Step Build 3: Signing
- Step-by-Step Build 4: App Bundles and Testing the Release Build
- Step-by-Step Build 5: R8 Shrinking, Obfuscation and Keep Rules
- Step-by-Step Build 6: Policy, Target API and Other Requirements
- Step-by-Step Build 7: Testing Tracks and the Pre-Launch Report
- Step-by-Step Build 8: The Release Checklist
- Guided Practice Exercises
- Common Mistakes (and How to Fix Them)
- Knowledge Check