skip to content

Shipping to Stores

Turning a Flutter project into signed store builds: flavors per environment, Android bundles and iOS archives, build numbers, obfuscation and CI. Interviewers probe it because release breaks late.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

30

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
open as a page

In a Flutter CI pipeline run on every merge, which Flutter and Dart commands form the stages, and what fails each one?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A Flutter pipeline runs flutter pub get --enforce-lockfile, dart format --output=none --set-exit-if-changed, flutter analyze, flutter test, then flutter build appbundle and flutter build ipa; each exits non-zero on drift, formatting, analyzer issues, failures or build errors.

open as a page

In Flutter, what does flutter build ipa produce, and how does that output reach App Store Connect?

level: juniorimportance: must knowfreq 55%

basics

~10 s

flutter build ipa archives the app in release mode into an .xcarchive under build/ios/archive, then exports a signed App Store .ipa into build/ios/ipa; you upload that .ipa with Transporter, xcrun altool or Xcode.

open as a page

In a Flutter pubspec.yaml, what does version: 1.2.3+4 become in the Android and iOS builds?

level: juniorimportance: must knowfreq 65%

basics

~10 s

The part before + (1.2.3) is the build name, used as Android versionName and iOS CFBundleShortVersionString; the part after + (4) is the build number, used as Android versionCode and iOS CFBundleVersion.

open as a page

How do you configure release signing for a Flutter Android app using an upload keystore, key.properties and signingConfigs in build.gradle.kts?

level: middleimportance: must knowfreq 55%

basics

~20 s

Create an upload keystore with keytool, describe it in android/key.properties (storePassword, keyPassword, keyAlias, storeFile), load that file in android/app/build.gradle.kts, create a release signingConfig from it, and point the release build type at it instead of the debug key.

open as a page

In Flutter, what is the difference between --flavor and --dart-define, and which do staging and production builds of a telemedicine app need?

level: middleimportance: must knowfreq 55%

basics

~20 s

--flavor selects a native build variant (an Android product flavor, an Xcode scheme) that can change the app ID, name and icon; --dart-define injects compile-time Dart constants such as the API URL. Two installable apps need a flavor; URLs can come from defines or appFlavor.

open as a page

For a Flutter iOS release, how do a distribution certificate and a provisioning profile differ, and when would you switch Xcode to manual signing?

level: middleimportance: must knowfreq 50%

basics

~20 s

A distribution certificate is the signing identity, usable only with its private key in the Mac's keychain; a provisioning profile is Apple's file binding an App ID, team, certificate and entitlements. Manual signing fits machines without an Apple account session.

open as a page

In Flutter, what do --obfuscate and --split-debug-info each do, and why does the tool refuse --obfuscate on its own?

level: middleimportance: must knowfreq 55%

basics

~10 s

--split-debug-info moves Dart debug symbols out of the app into app.<arch>.symbols files; --obfuscate renames Dart identifiers. Obfuscate requires split-debug-info because those files hold the mapping needed to read obfuscated stack traces.

open as a page

For a Flutter car-wash booking app built on every merge, how does CI get the Android upload keystore and the iOS distribution certificate without committing them?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Store the keystore and certificate as encrypted CI secrets, materialise them only during the job: decode the base64 keystore and write key.properties for Gradle; import the .p12 and profiles into a temporary keychain on the macOS runner, then sign.

open as a page

You ship a redesigned home screen in a Flutter pharmacy app to Google Play and the App Store; why stage the release, and what can halting the rollout not undo?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Release Dart is AOT-compiled into the binary, so every fix is a new reviewed build; staging limits who meets a bad one. A halt cannot downgrade users who updated: recover with a higher build number or a runtime switch.

open as a page

In Flutter, how does Dart code learn which flavor it was built with, and what does appFlavor hold when no --flavor is passed?

level: juniorimportance: should knowfreq 38%

basics

~10 s

Import package:flutter/services.dart and read the appFlavor constant; it equals the --flavor name, or the pubspec's default-flavor when none is passed, and is null when neither is set.

open as a page

A Flutter church-events app's App Store build is rejected for a missing purpose string; what is a purpose string, and where do you add it?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A purpose string is an NS...UsageDescription key, such as NSCameraUsageDescription, in ios/Runner/Info.plist whose text iOS shows in the permission prompt; each protected resource the app uses needs one, and Flutter plugins never add it for you.

open as a page

Why might App Store or Google Play review reject a Flutter app that requires a login, and what do you give the reviewers?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Reviewers must reach every feature, so a login wall without working credentials gets the app rejected. Give a demo account in App Store Connect's App Review Information and Play Console's App access, valid on the release build's backend.

open as a page

What does minSdk = flutter.minSdkVersion mean in a Flutter app's build.gradle.kts, and when should you replace it with a fixed number?

level: middleimportance: should knowfreq 32%

basics

~20 s

flutter.minSdkVersion is the minimum Android API level the Flutter SDK supplies through its Gradle plugin, 24 in Flutter 3.47. Replace it with a higher fixed number when a plugin requires one, or pin it to stop upgrades moving it.

open as a page

What does a Flutter-aware CI service like Codemagic or Bitrise handle that GitHub Actions with subosito/flutter-action leaves to you?

level: middleimportance: should knowfreq 35%

basics

~20 s

Codemagic and Bitrise are all-in-one Flutter CI services: they select the Flutter version, provide macOS machines, manage signing files and publish to stores or Firebase App Distribution. With GitHub Actions and subosito/flutter-action you script those steps yourself, often with fastlane.

open as a page

In Flutter CI, how do you pin the SDK and reuse the pub cache, and why is pubspec's environment: flutter constraint not a pin?

level: middleimportance: should knowfreq 40%

basics

~20 s

Pin the Flutter SDK by installing an exact release in the pipeline, for example through FVM or an exact version on the setup step; environment: flutter only enforces a lower bound. Cache the pub cache ($HOME/.pub-cache or PUB_CACHE) between runs.

open as a page

In Dart, how do you read a value passed with Flutter's --dart-define, and why must String.fromEnvironment be invoked as const?

level: middleimportance: should knowfreq 45%

basics

~20 s

Read it with const String.fromEnvironment('API_URL', defaultValue: ...), or bool.fromEnvironment and int.fromEnvironment. The value is fixed by the compiler, so only a const invocation is guaranteed to see it; ahead-of-time builds have no define table at runtime.

open as a page

In a Flutter iOS app, what is PrivacyInfo.xcprivacy, and which parts of the app are expected to ship one?

level: middleimportance: should knowfreq 30%

basics

~10 s

PrivacyInfo.xcprivacy is Apple's privacy manifest, a plist declaring tracking, collected data types and required-reason API use; Flutter's engine framework ships one, plugins ship their own, and the app adds one for its own code.

open as a page

Google Play blocks your Flutter app's update for an old target API level; where does Flutter set targetSdk, and what does raising it risk?

level: middleimportance: should knowfreq 36%

basics

~20 s

The app's build.gradle.kts sets targetSdk = flutter.targetSdkVersion, supplied by the Flutter Gradle plugin (36 in Flutter 3.47), so upgrading Flutter raises it. Raising it opts the app into that Android version's behaviour changes, such as enforced edge-to-edge, which you must test.

open as a page

In a Flutter Android release, why can Google Play flag a permission your AndroidManifest.xml never declares, and how do you remove it?

level: middleimportance: should knowfreq 42%

basics

~20 s

Gradle's manifest merger folds every plugin's AndroidManifest.xml into the app's, so plugin permissions ship in your bundle. Inspect the merged manifest, then drop unused ones with tools:node="remove", but only when no code path still needs them.

open as a page

When filling Google Play's Data safety form and Apple's App Privacy details for a Flutter app, why audit plugins rather than just your Dart code?

level: middleimportance: should knowfreq 32%

basics

~20 s

Both forms must declare data collected by everything in the binary, including third-party SDKs. Flutter plugins wrap native SDKs that collect data your Dart code never touches, so the inventory starts from the full dependency tree, not your own calls.

open as a page

In Flutter, what do --build-name and --build-number override, and what build number ships when pubspec's version line has no + part?

level: middleimportance: should knowfreq 40%

basics

~20 s

--build-name and --build-number replace the two halves of pubspec's version for one build. Without a + part, Android's versionCode falls back to 1 and iOS's CFBundleVersion falls back to the build name, such as 1.2.3.

open as a page

Under Play App Signing, what is the difference between a Flutter app's upload key and app signing key, and what happens if the team loses the upload keystore?

level: seniorimportance: should knowfreq 40%

basics

~20 s

You sign uploads with an upload key; Google Play signs the APKs users install with an app signing key it holds. A lost upload keystore is replaced by registering a new upload key, since the key devices trust never left Google.

open as a page

When a Flutter release build crashes at startup in a plugin's Android code while debug works, how can R8 cause it, and how do keep rules fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Flutter's Gradle plugin enables R8 for release builds, and R8 removes or renames JVM classes it cannot see used, such as ones reached by reflection or JNI. Add -keep rules to android/app/proguard-rules.pro, which the plugin applies automatically.

open as a page

For a Flutter telemedicine app with staging and production, would you use separate entry points with -t, or one main.dart driven by appFlavor and define files, and why?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Prefer one main.dart that reads appFlavor and const defines from a per-environment file, validated at startup; separate main_staging.dart entry points chosen with -t work, but duplicate wiring and add a third switch that can disagree with --flavor.

open as a page

When flutter build ipa --flavor staging fails with 'You must specify a --flavor option to select one of the available schemes', how must the Xcode project be set up?

level: seniorimportance: should knowfreq 32%

basics

~20 s

The Xcode project needs a shared scheme whose name matches the flavor, plus build configurations named Debug-staging, Profile-staging and Release-staging assigned to that scheme's actions; per-configuration settings then give the flavor its own bundle ID and name.

open as a page

Why would a Flutter iOS app built with the Xcode 27 SDK crash at launch, and what does the UIScene migration change in its iOS project?

level: seniorimportance: should knowfreq 22%

basics

~20 s

Apple requires the UIScene lifecycle for UIKit apps built with the iOS 27 SDK, and a Flutter app that has not adopted it crashes at launch; migration adds a scene manifest to Info.plist and moves plugin registration out of didFinishLaunching.

open as a page

A crash report from an obfuscated Flutter sports-betting app shows only addresses; how do you turn it into a readable Dart stack trace?

level: seniorimportance: should knowfreq 35%

basics

~10 s

Save the trace to a file, find the symbols file from that exact build and architecture, and run flutter symbolize -i trace.txt -d app.android-arm64.symbols; without the matching file the trace cannot be decoded.

open as a page

Why might App Review reject a Flutter app that is mostly one webview_flutter WebViewWidget showing your website, and what helps it pass?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

Apple's minimum-functionality rule rejects apps that are little more than a repackaged website. A Flutter shell around one WebViewWidget looks exactly like that; building core flows as Flutter screens with app-only value such as offline use or notifications helps it pass.

open as a page

After moving a Flutter Android app to Android Gradle Plugin 9, what do android.builtInKotlin and android.newDsl in gradle.properties do, and what migration does the app need?

level: seniorimportance: nice to knowfreq 22%

basics

~10 s

AGP 9 compiles Kotlin itself and knows only its new DSL; Flutter's android.builtInKotlin=false and android.newDsl=false are temporary escape hatches. Migrating removes kotlin-android and kotlinOptions and adds kotlin { compilerOptions { ... } }.

open as a page