skip to content

In a React Native Android release, what is the difference between an AAB and an APK, and which one does Google Play expect?

level: juniorimportance: must knowfreq 65%

answer

  1. what you upload vs what installs
  2. bundleRelease vs assembleRelease
  3. Play generates per-device split APKs
  4. AAB requires Play App Signing
  5. APK still useful off-store

basics

~20 s

An APK is the package a device installs; an AAB (Android App Bundle) is a publishing format Google Play turns into per-device APKs. React Native's release command runs bundleRelease to build the AAB Play requires for new apps.

solid answer

~40 s

An APK is the file a device actually installs. An **AAB** (Android App Bundle) is a publishing artifact: you upload it and Google Play generates optimized split APKs per device, carrying only the CPU architecture, screen density and language resources that device needs. In a React Native project, `npx react-native build-android --mode=release` runs Gradle's `bundleRelease` and writes `android/app/build/outputs/bundle/release/app-release.aab`, while `./gradlew assembleRelease` produces an APK. Both release variants embed the Metro JS bundle, so neither needs Metro at runtime. Google Play requires AABs for new apps and accepts them only with Play App Signing configured, because Play re-signs the APKs it generates. APKs remain the tool for sideloading, QA devices and stores that do not take bundles.

code

bash · 9 lines
bash
# Play upload artifact (AAB) via Gradle bundleRelease
npx react-native build-android --mode=release
# -> android/app/build/outputs/bundle/release/app-release.aab

# Installable APK for QA devices or other stores
cd android && ./gradlew assembleRelease

# Build, install and launch the release variant on a connected device
npm run android -- --mode="release"

go deeper

for a junior

Know which file you upload (the .aab from bundleRelease) and which file a phone installs (an .apk). Remember the release command and where the output lands.

for a middle

Explain that Play builds per-device split APKs from the bundle, which is why it must hold the app signing key, and that release variants embed the JS bundle instead of using Metro.

for a senior

Reason about what an AAB does and does not shrink in a React Native app: native ABIs and density resources yes, the single JS bundle no. Diagnose a release that ships without its bundle.

for a principal

Weigh distribution channels together: an AAB for Play, APKs for off-store QA or other stores, and the signing and versioning consequences each channel adds to the release process.

## Two formats, two jobs Android has two packaging formats a React Native developer meets at release time, and they answer different questions: **what does a device install?** and **what does a store receive?** | | APK | AAB (Android App Bundle) | |---|---|---| | What it is | An installable package | A publishing artifact, not installable as-is | | Gradle task | `assembleRelease` | `bundleRelease` | | Output path | `android/app/build/outputs/apk/release/` | `android/app/build/outputs/bundle/release/app-release.aab` | | Who installs it | A device, directly | Google Play turns it into split APKs per device | | Native code for all ABIs | In one universal file unless you split | Play delivers only the device's ABI | | Signing | Signed with your key, installed as signed | Signed with your upload key, re-signed by Play | The key idea: an **APK** is the unit of installation, while an **AAB** is the unit of publication. Google Play requires the AAB format for new apps, so for a Play release the AAB is the artifact you build. ## How a React Native project builds each The React Native docs for 0.87 describe the Play path as one command: 1. Configure release signing with an upload keystore (a separate question on this leaf). 2. Run `npx react-native build-android --mode=release`. It calls Gradle's `bundleRelease` task under the hood. 3. Pick up `android/app/build/outputs/bundle/release/app-release.aab` and upload it to the Play Console. For an APK you run `./gradlew assembleRelease` from the `android/` folder instead. To try the release variant on a phone, `npm run android -- --mode="release"` builds, installs and launches it; the docs note that `--mode release` only works once signing is set up. ## What ends up inside a release artifact A release variant differs from the debug build you run every day: - **The JavaScript bundle is embedded.** The React Native Gradle plugin bundles JS for every variant *not* listed in `react { debuggableVariants }`, whose default is `debug` and `debugOptimized`. A release artifact therefore carries `index.android.bundle` in its assets and never talks to Metro. - **Images you `require()` become Android resources.** The bundling task writes them into a generated `res` folder, so they are packaged like any drawable, per screen density. - **Native libraries per ABI.** React Native ships compiled `.so` libraries; the docs note a default APK carries code for `x86`, `x86_64`, `armeabi-v7a` and `arm64-v8a`. - **The signing certificate** of whoever signed the artifact. ## Why an AAB needs Play App Signing With an APK, the signature you apply is the signature the user's device checks. With an AAB, Google Play builds the final APKs itself, so it must sign them itself. That is **Play App Signing**: Google holds the *app signing key*, you sign your uploads with an *upload key*, and Play verifies the upload before re-signing the generated APKs. The React Native docs state it plainly: Play accepts the AAB format only when App Signing by Google Play is configured for the app. ## The scenario: a language-learning app's first Play release Imagine the first release of a language-learning app with lessons in twelve languages and audio for each. - Uploading an AAB means a phone with an `arm64-v8a` CPU and a high-density screen downloads only that ABI's native code and that density's drawables, which keeps the download small. - **Language splits apply to Android string resources** (`res/values-*`). The lesson text your JS imports is inside `index.android.bundle`, so every device gets all of it; to shrink that you would have to load translations at runtime instead. - The QA team, testing on devices outside Play, gets an APK from `assembleRelease`, because an AAB cannot be installed by tapping it. ## Common traps - **Installing the AAB directly.** It is not an installable package; build an APK or use the release run command for local testing. - **Uploading a debug build.** Debug variants load JS from Metro and are signed with the debug keystore; Play refuses them. - **`org.gradle.configureondemand=true` in `gradle.properties`.** The React Native docs warn it makes the release build skip bundling JS and assets, so the app starts with no bundle. - **Assuming the AAB changes the JS side.** Bundle format affects native code, resources and signing; the JS bundle ships whole either way.

  • A React Native release AAB installs from Play but shows 'Unable to load script'; what Gradle setting is a known cause?
    `org.gradle.configureondemand=true` in `gradle.properties`. The React Native docs warn that it makes the release build skip bundling JS and assets, so the binary has no `index.android.bundle` and looks for Metro like a debug build. Remove it and rebuild. Also check that the variant is not listed in `react { debuggableVariants }` (default `debug` and `debugOptimized`), because listed variants are never bundled.
  • When would a React Native team still build ABI-split or universal APKs?
    For stores or channels that take APKs rather than bundles. The docs show an `android { splits { abi { ... } } }` block: `universalApk false` produces one APK per CPU architecture for stores with device targeting, `true` adds one universal APK. Each split APK needs its own distinct `versionCode`, so the version scheme must encode the ABI.
  • Why do release images from require() get a per-density benefit from an AAB, while JS strings do not?
    The React Native Gradle plugin writes required images into a generated `res` folder, so they are ordinary Android drawables that Play can split by screen density. Strings imported by JavaScript are compiled into `index.android.bundle`, a single asset file, so Play cannot split them by language and every device downloads them all.

An AAB is a shop's master catalogue sent to a print house; the print house (Google Play) prints each customer a booklet with only their size and language. An APK is the printed booklet itself, which you can hand to someone directly.

saying these in an interview costs you the question

  • An AAB can be installed on a phone by tapping it, like an APK.
  • The debug build from npm run android is what you upload to Play.
  • A release AAB downloads its JavaScript from Metro on first launch.
  • Play serves every device the same universal APK built from the AAB.
  • You can upload an AAB to Play without Play App Signing.
  • Choosing AAB lets Play split the app's JS translations per language.