APK vs AAB in Flutter is not just a file format question. It affects how fast QA can test your build, how you deliver staging/internal builds, how Google Play ships to real users, and how your CI pipeline should be structured. This guide explains what each format is, how to build them in Flutter, and how teams actually use them in practice.
Audience: IntermediateTested on: Flutter 3.x, Dart 3.x, Android 14, macOS/Windows
- APK vs AAB: what they are in plain English
- Why teams ship both, not just one
- Setting up signing (do this once)
- How to build an APK (for QA and DeployGate)
- How to build an AAB (for Google Play)
- Versioning, symbols, and crash debugging
- CI pipeline: one job for QA APK, one job for store AAB
- Security, signing, and other pitfalls
- FAQ
- Conclusion
APK vs AAB: what they are in plain English
APK (Android Package) is the installable app file. You can give it directly to QA, PMs, or clients. They download it, tap it, and it’s on the device. This is perfect for fast feedback loops and distribution through tools like DeployGate. An APK is what people sideload.
AAB (Android App Bundle) is not directly installable. You upload the AAB to the Google Play Console, and Google Play generates device-specific APKs for each user (correct CPU architecture, correct screen density, correct language resources). For production and most staged rollouts, Google expects an AAB instead of a universal APK. You can read more in Google’s app bundle overview.
Why teams ship both, not just one
In real projects you typically have two tracks going at the same time:
- Fast loop / QA loop: You build an APK and push it to something like DeployGate. QA and stakeholders can install it immediately. No Play Console, no review, no tracks, no waiting. Perfect for “can you check this fix now?” situations.
- Store-ready loop: You build an AAB and upload it to an Internal testing or Closed testing track in Google Play. Testers receive the app from the Play Store itself. That means you also validate signing, Play Protect, billing permissions, update flow, install/upgrade behavior, etc. This is how you de-risk production.
The pattern is: APK for speed, AAB for realism.
Setting up signing (do this once)
Both APK and AAB need to be signed. A typical setup looks like this:
# 1) Generate a keystore
keytool -genkey -v -keystore my-release-key.jks -alias my-key-alias \
-keyalg RSA -keysize 2048 -validity 10000
# 2) Put credentials in android/key.properties
storePassword=********
keyPassword=********
keyAlias=my-key-alias
storeFile=../my-release-key.jks
Then in android/app/build.gradle (or build.gradle.kts), reference that keystore in signingConfigs.release and make sure buildTypes.release uses it. Treat this file like a secret. In CI, you typically inject these values via environment variables instead of committing them.
How to build an APK (for QA and DeployGate)
The APK is what you’ll hand to testers directly or upload to DeployGate. You should always build it in --release mode, not debug, so performance and behavior are realistic.
# Universal APK (one file that works on most devices)
flutter build apk --release
# Split-by-ABI APKs (one per CPU architecture; smaller per file)
flutter build apk --release --split-per-abi
Universal APK: easiest for non-technical testers, but larger in size because it contains multiple ABIs. –split-per-abi: produces multiple APKs (arm64-v8a, armeabi-v7a, etc.) so each one is smaller. The downside: you need to hand the right file to the right person/device.
Once built, you can upload the APK to a distribution service like DeployGate and share a link. This is ideal for daily/adhoc QA.
How to build an AAB (for Google Play)
The AAB is what goes into Google Play Console. It’s the format Google will use to generate optimized APKs per device and deliver them to testers or to production users.
# AAB for Play Console (Internal/Closed testing, Production, etc.)
flutter build appbundle --release
You’ll upload the resulting .aab to Google Play Console. Testers in the Internal or Closed testing track can then install from the Play Store UI. This gives you a “real store install” experience before going live. See Flutter’s official Android deployment guide for more context.
Versioning, symbols, and crash debugging
Before every upload to Play, bump versionCode and versionName in android/app/build.gradle so Play accepts the new build. versionCode must strictly increase.
For production or wide testing, you should also obfuscate and keep symbol files:
flutter build appbundle --release \
--obfuscate \
--split-debug-info=out/symbols/2025-10-30T210000Z
The --obfuscate flag shrinks/obfuscates Dart symbols, and --split-debug-info writes mapping files. Store those mapping files somewhere safe so you can decode crash logs later. This is part of a real release pipeline, not just a “toy” build.
CI pipeline: one job for QA APK, one job for store AAB
A very common GitHub Actions / CI setup looks like this:
- name: Install dependencies
run: |
flutter pub get
- name: Lint & static analysis
run: |
dart analyze --fatal-infos --fatal-warnings
- name: Tests
run: |
flutter test --coverage
- name: Build QA APK (for DeployGate)
run: |
flutter build apk --release --split-per-abi \
--dart-define-from-file=tool/env/qa.json
- name: Build Play AAB (for Internal test / Production)
run: |
flutter build appbundle --release \
--dart-define-from-file=tool/env/prod.json \
--obfuscate \
--split-debug-info=out/symbols/${{ github.run_id }}
Key idea: CI always produces two artifacts per commit/tag. One is the APK you push to QA immediately. One is the AAB you can upload to Google Play Internal testing. This pattern keeps feedback fast without skipping the reality check of Play distribution.
Security, signing, and other pitfalls
- Debug builds lie. Never judge performance, cold start time, or jank using a debug APK. Always hand QA a
--releaseAPK. - Wrong ABI for split APKs. A tester might say “App not installed.” That usually means you gave them an APK built for a different CPU architecture. Universal APK avoids this problem at the cost of size.
- Upload key safety. If you’re using Play App Signing, Google manages the final signing key for production devices, but you still need to protect your upload key. If you lose it, you have to rotate through Google Play Console, which is annoying and slows releases.
- 64-bit requirement. Google Play enforces 64-bit (arm64-v8a). Make sure your builds include it. The 64-bit requirement docs explain this policy.
- Store behavior ≠ sideload behavior. Sideloaded APK and Play-installed AAB can behave slightly differently around permissions, in-app billing, Play Protect, etc. Always run through an Internal testing track before going live.
FAQ
Q: Can I skip AAB and just ship APK to real users?
A: For new Play Store production releases: not really. Google Play expects an AAB. APK is still great for side-loaded distribution (DeployGate, direct download, etc.) but not for publishing production builds to the store.
Q: Which file should PMs / non-dev testers use daily?
A: The APK. It’s fast to generate and easy to install. You can hand them a link 10 minutes after you merge a fix.
Q: Which file should we attach to a release candidate ticket?
A: The AAB. Upload it to Internal testing in Play Console and ask testers to install via the Play Store. That simulates the exact production environment.
Conclusion
In a healthy Flutter release flow, APK and AAB are not competitors. They’re tools for two different checkpoints: the APK accelerates QA and daily feedback, and the AAB validates that Google Play can actually deliver what you think you’re shipping. Your CI should output both, every time, so you can move fast without gambling on release day.
Updated: 2025-10-30


Comment