skip to content

For a Flutter app's first Google Play release, why build with flutter build appbundle rather than flutter build apk, and when does --split-per-abi matter?

level: juniorimportance: must knowfreq 60%

answer

  1. Play builds per-device APKs
  2. new Play apps must upload a bundle
  3. fat APK carries every ABI
  4. three ABIs in a release build
  5. split APKs bump versionCode per ABI

basics

~20 s

flutter build appbundle produces an .aab from which Google Play generates per-device APKs, and new Play apps must upload a bundle; --split-per-abi matters only when shipping APKs elsewhere, replacing one fat APK with one APK per ABI.

solid answer

~40 s

`flutter build appbundle` (release by default) writes `build/app/outputs/bundle/release/app.aab`, holding Dart AOT code and the engine for `armeabi-v7a`, `arm64-v8a` and `x86_64`. Play splits it and serves each device only its ABI and resources, and new Play apps can no longer be submitted as APKs. `flutter build apk` alone yields a fat APK with all three ABIs, which is larger for everyone; `flutter build apk --split-per-abi` writes one APK per ABI and adds `ABI_VERSION * 1000` to each `versionCode` so they do not collide. So for a plant-care app's first Play release you upload the bundle, and keep split APKs for other stores, direct downloads or testers installing by hand.

code

bash · 9 lines
bash
# Upload to Google Play
flutter build appbundle
# build/app/outputs/bundle/release/app.aab

# Distribute outside Play, one APK per CPU family
flutter build apk --split-per-abi
# build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
# build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
# build/app/outputs/flutter-apk/app-x86_64-release.apk

go deeper

for a junior

Remember: flutter build appbundle for Google Play, flutter build apk for installing directly, and --split-per-abi to avoid one fat APK outside Play.

for a middle

Explain what Play does with a bundle (per-device APKs), which three ABIs Flutter builds, and why split APKs get per-ABI version codes.

for a senior

Choose artifacts per channel, test bundles with bundletool or internal tracks before release, and keep version codes consistent across bundles and APKs.

for a principal

Decide which distribution channels the app supports and what each costs in build variants, signing and support for users outside Play.

## Two artifacts, two audiences A Flutter Android release can be packaged two ways: - An **Android App Bundle** (`.aab`) — a publishing format. You upload it; the store builds the installable APKs from it. - An **APK** (`.apk`) — the installable package itself. You can hand it to a device directly. The commands: ```bash flutter build appbundle # -> build/app/outputs/bundle/release/app.aab flutter build apk # -> one "fat" APK with every ABI flutter build apk --split-per-abi # -> one APK per ABI ``` `flutter build` defaults to **release** mode, so none of these needs `--release`. ## What goes inside A Flutter release build contains native code: the Flutter engine and your Dart code compiled ahead of time. Native code is per **ABI** (application binary interface, the CPU architecture family). Flutter's Android release builds target three ABIs: - `armeabi-v7a` (32-bit ARM), - `arm64-v8a` (64-bit ARM), - `x86_64` (64-bit x86, mostly emulators and some devices). By default the app bundle contains all three. ## Why the bundle for Google Play When you upload an `.aab`, Google Play generates **optimised APKs per device configuration**: a phone with an `arm64-v8a` CPU downloads only the `arm64-v8a` native libraries and the resources for its screen density and language. Users download less than they would with a fat APK. There is also a hard rule: Google Play no longer accepts **new apps** submitted as APKs, so the first release of a plant-care app has to be a bundle. Uploading a bundle also means enrolling in **Play App Signing**, where Google holds the key that signs the APKs it generates and you sign uploads with your own upload key. Before uploading, you can test a bundle offline with Google's `bundletool`, which generates the same device-specific APKs locally, or online through Play's internal testing tracks. ## When APKs still matter 1. **Stores or channels without bundle support** — other app stores, an enterprise MDM, a direct download from your website. 2. **Hand-installed test builds** — sending a build to a tester, or `flutter install` onto a connected device. 3. **Size-sensitive distribution outside Play** — here `--split-per-abi` matters. A plain `flutter build apk` produces a **fat APK**: one file with native code for all three ABIs. It installs anywhere but every user downloads code for CPUs they do not have. `flutter build apk --split-per-abi` instead writes: - `app-armeabi-v7a-release.apk` - `app-arm64-v8a-release.apk` - `app-x86_64-release.apk` ## The version-code detail Stores that accept several APKs for one app require each APK to have a **distinct version code**. When you split per ABI, Flutter therefore adds `ABI_VERSION * 1000` to the `versionCode` derived from `pubspec.yaml`. If you need the plain version code for every split, the Flutter docs describe forcing it with the `force-version-code-ignoring-abi=true` Gradle property. With an app bundle this does not arise: one bundle, one version code. | | `appbundle` | `apk` | `apk --split-per-abi` | |---|---|---|---| | Upload to Google Play (new app) | yes | no | no | | Install directly on a device | no (needs `bundletool`) | yes | yes, matching ABI | | Native code per file | all ABIs, split by Play | all ABIs | one ABI | | `versionCode` | from pubspec | from pubspec | pubspec + `ABI_VERSION * 1000` | ## Where the build lands Flutter writes Android release artifacts under `build/app/outputs/`: the bundle in `bundle/release/app.aab` and APKs in `flutter-apk/`. With product flavors the flavor name appears in the file name, for example `app-staging-release.apk`. CI jobs usually collect these paths as build artifacts; a wrong glob is a common reason a pipeline "succeeds" but uploads nothing. ## Things interviewers probe - **"Why is my APK so big?"** — it is a fat APK; users of a bundle-delivered app download far less. - **"Can I sideload the `.aab`?"** — not directly; an `.aab` is not installable without `bundletool` generating APKs from it. - **"Does the bundle change what my Dart code does?"** — no; it changes packaging and delivery, not the compiled program. - **Signing still applies** to both formats: an unsigned or debug-signed bundle is rejected on upload, which is the next thing to configure.

  • Why does Flutter add ABI_VERSION * 1000 to the versionCode of split APKs?
    Stores that accept multiple APKs for one app require each APK to carry a distinct version code. Adding a per-ABI offset to the pubspec's build number makes the three split APKs unique; the Flutter docs describe a Gradle property to force the plain version code instead.
  • How can you check what a device will actually receive from an app bundle before uploading it?
    Use Google's `bundletool` to generate the device-specific APK set from the `.aab` locally and install it on a device, or upload to an internal testing track and install from Play. Both exercise the same split that real users get.

saying these in an interview costs you the question

  • Uploads a fat APK for a new Google Play app
  • Thinks an .aab file can be installed directly on a phone
  • Believes --split-per-abi is needed when uploading an app bundle
  • Adds --release to flutter build, not knowing release is the default
  • Thinks the bundle format changes how the Dart code runs