Build for Android
Create the upload key and android/key.properties, choose between an APK and an App Bundle, set the version name and build number, wire Firebase to the right application ID, and make the first Google Play upload.
An Android build that Google Play will accept needs three things the package cannot ship for you: your own application ID, your own upload key, and a build number Play has not seen before. The build itself is one Flutter command. Everything on this page is about making sure what comes out of it is yours and uploadable.
This page assumes the app already runs against your server. If it does not, start with Install and run the app.
Before you start
- The app runs in a debug build against your server
-
androidApplicationIdset inassets/config/app_config.json(see Branding) - A JDK on your machine (it provides
keytool) - A Google Play developer account for your organisation (see Developer accounts)
The application ID comes from app_config.json
Android and Google Play know your app by its application ID. Gradle reads
it from androidApplicationId in assets/config/app_config.json on every
build. Left out, it is com.bicrypto.mobile, the template's own ID.
{
"androidApplicationId": "com.myexchange.app"
}Nothing else needs to run. You do not edit build.gradle.kts or the manifest.
The same Gradle step reads appName for the name under the icon, and names
what it used:
App identity from assets/config/app_config.json: name="My Exchange" applicationId=com.myexchange.appFlutter runs Gradle quietly, so that line appears only when you add
--verbose to the flutter build command. If app_config.json is missing or
empty, Gradle builds with the example file's values instead and the line ends
with (assets/config/app_config.json is missing or empty, so the tracked
template was used). If the file is not valid JSON, the build stops and gives
the line and column.
Changing the application ID makes a different app. Play will not accept
it as an update to your existing listing, and phones with the old app never
update to it. Your Firebase google-services.json must also be reissued for
the new ID. Branding covers the format rules and
the consequences.
Create your upload key
Google Play accepts only builds signed with your key. The project reads
that key from android/key.properties, a file that does not exist until you
create it.
-
Create a keystore with
keytool, which comes with the JDK. Keep it outside the project folder, so it can never end up in a copy of the source you share or commit. These commands put it in your home folder:# macOS and Linux keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload # Windows (PowerShell) keytool -genkey -v -keystore $env:USERPROFILE\upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias uploadIt asks for a password and for your name and organisation. Unless it also asks for a separate key password, the key's password is the keystore's password, and the same value goes in both password lines below. Write it down somewhere safe now.
-
Create
android/key.properties. It sits in theandroid/folder, next tosettings.gradle.kts, and not inandroid/app/. It needs exactly these four keys:storeFile=/Users/you/upload-keystore.jks storePassword=your-keystore-password keyAlias=upload keyPassword=your-key-passwordUse an absolute path for
storeFile. A relative path is resolved fromandroid/app/, which is rarely what you meant. On Windows, write the path with forward slashes (C:/Users/you/upload-keystore.jks), because a backslash in a.propertiesfile starts an escape. -
Check it. The preflight reads
key.properties(never the keystore itself) and refuses a missing file or a missing key:dart run tool/release_preflight.dart --explain -
Back the keystore up, with its passwords, somewhere you will still have it in three years. Every upload to your listing is signed with it, and for an app not enrolled in Play App Signing, losing it means that listing can never be updated again.
android/key.properties, *.jks and *.keystore are already git-ignored in
android/.gitignore. Keep it that way. The file holds your passwords, and the
keystore is the one secret in this whole process.
A key.properties that exists but lacks one of storeFile, storePassword,
keyAlias or keyPassword fails while Gradle is configuring the project, with
an error that names none of them. The preflight names the missing one:
android/key.properties has no value for keyAlias.
What happens without key.properties
The release build does not fail. It signs itself with the Android SDK's
debug key, so that flutter run --release still works on your own machine. Gradle
prints a warning:
WARNING: android/key.properties not found — signing the release build with the DEBUG keystore. This artifact cannot be uploaded to Google Play. See the comment at the top of this file.Flutter runs Gradle quietly and that warning is not shown without --verbose,
so a plain flutter build appbundle --release finishes cleanly and gives you a
file that looks ready. Play refuses it at upload. The preflight is the check that tells
you first: android/key.properties is missing, so a release build is signed
with the DEBUG key.
A debug-signed APK also causes a problem on phones. Android only installs an update signed with the same key as the copy already installed, so a phone with a debug-signed test copy will not accept your properly signed build, and a phone with the Play copy will not accept a build you sideload. Uninstall the old copy first whenever you switch between them.
To see which key signed a file:
# An App Bundle
keytool -printcert -jarfile build/app/outputs/bundle/release/app-release.aab
# An APK (apksigner is in the Android SDK's build-tools folder)
apksigner verify --print-certs build/app/outputs/flutter-apk/app-release.apkA debug-signed file reports CN=Android Debug. Yours reports the name you gave
keytool.
APK or App Bundle
| APK | App Bundle (AAB) | |
|---|---|---|
| Command | flutter build apk --release |
flutter build appbundle --release |
| Output | build/app/outputs/flutter-apk/app-release.apk |
build/app/outputs/bundle/release/app-release.aab |
| Installs on a phone | Yes, directly | No. Play turns it into per-device APKs. |
| Use it for | Testing on your own phones, and distribution outside Google Play | Every upload to Google Play |
Google Play takes new apps as App Bundles, so the AAB is the file you upload. Build an APK when you want to hand a build to a tester's phone.
An App Bundle is larger than anything a phone downloads. Part of it is
BUNDLE-METADATA, the R8 mapping and the native debug symbols, which Play
keeps for reading crash reports and never sends to a phone.
The Dart code is not obfuscated by default. If you want it, add
--obfuscate --split-debug-info=<folder> to the build command and keep that
folder for every release you ship, because crash stack traces from that build
cannot be read without it.
Version name and build number
Both numbers come from one line in pubspec.yaml:
version: 5.3.9+10The part before + is the version name customers see (versionName,
5.3.9). The part after it is the build number (versionCode, 10). You
can also set them on the command line without editing the file:
flutter build appbundle --release --build-name=1.2.0 --build-number=14Google Play refuses an upload whose build number it has already received, and
Android will not install a lower build number over a higher one. Raise the
number after + for every upload, including a rebuild of the same
version. Raise the version name whenever the release means something to a
customer.
The trap is a new source release from us. It arrives with our version:
line. If your own uploads have already gone past that build number, set
--build-number above your last upload before you build it. See
Updating your app.
The version your customers can quote to support is shown in the app itself,
at Profile (the gear icon at the top right of Home) → Open-source
Licenses, as version and build number together, for example 5.3.9 (10).
It is read from the build. The appVersion key in app_config.json is only a
fallback for test environments that cannot ask the platform, so editing it does
not change what a built app shows.
Merchant, AI Investment and Affiliate
A normal build leaves three modules out entirely. To compile them in, add one flag to the build command:
flutter build appbundle --release --dart-define=STORE_RESTRICTED_MODULES=trueThis is the only build flag the app reads. Everything else, from your server's
address to your keys, comes from app_config.json, and the Android project has
no product flavours.
The flag is a store decision, not a technical one. AI Investment and Affiliate also need a server switch that ships off, and all three carry rejection risk under your account. Read Restricted modules before you use it.
Push notifications: the Firebase file must match the ID
Push on Android is switched on by one file, android/app/google-services.json,
from your own Firebase project. The build applies Firebase only when that file
is there, and says which way it went (visible with --verbose):
google-services.json found - Firebase push ENABLED
google-services.json not found - Firebase push DISABLED. The app builds and runs; see the note in android/app/build.gradle.kts to enable it.Without the file the app builds, runs and publishes normally, with no push.
With it, the file must come from a Firebase Android app registered with your
androidApplicationId. A file issued for any other ID stops the build with
No matching client found for package name. The fix is to add an Android app
with your ID in the Firebase console and download its file into android/app/.
Push also needs the server half. See Push notifications.
Upload to Google Play
-
Run the preflight and fix everything it prints. It covers the name, the IDs, the icon, the colours, the signing key, the target API, a server address still left on the placeholder, and an
http://orws://address ([backend-not-encrypted]), which a release build refuses on its first screen.dart run tool/release_preflight.dart --explainIt also checks the iOS half of the project, including an Apple signing team, which the package does not carry. If you publish on Android only, add
--android-only: the iOS checks are skipped and every Android check still runs.dart run tool/release_preflight.dart --explain --android-only -
Raise the build number above your last upload, then build the bundle:
flutter build appbundle --release -
Create the app in Play Console under your organisation's account, then upload
app-release.aabto the Internal testing track first. New apps use Play App Signing: Google holds the key that signs what reaches phones, and the key you created above is your upload key. -
Install it from the testing track on a real phone and sign in with a real account before you promote anything.
-
Complete the declarations Play asks for before a production release. The ones specific to this app are covered in Developer accounts and Data safety and privacy. The reviewer needs a working login; see App review access.
The project already targets API 36, which Google Play requires for new apps and
for updates from 31 August 2026. Do not lower compileSdk or targetSdk to
make an older Android SDK work; install API 36 instead. The preflight refuses a
target below 36.
Build messages
The first two lines below are printed only when you build with --verbose.
Every other message stops the build or the command, so it always shows.
| Message | What to do |
|---|---|
WARNING: android/key.properties not found — signing the release build with the DEBUG keystore. |
Create the upload key and key.properties above. Nothing you build without it can go to Play. |
google-services.json not found - Firebase push DISABLED. |
Nothing, unless you want push. See Push notifications. |
assets/config/app_config.json is not valid JSON (at line … column …) |
Fix the file at that place. The usual causes are a comma after the last entry, a missing comma between two entries, or a missing quote. |
No matching client found for package name |
google-services.json was issued for a different application ID. Download one for your androidApplicationId. |
flutter.sdk not set in local.properties, or an error that local.properties cannot be found |
Run flutter pub get once in the project folder. It writes android/local.properties, which the package does not ship. This usually shows when Gradle is run from Android Studio or gradlew before any flutter command. |
| A build error naming Android SDK Platform 36 or NDK 27.0.12077973 | Install that exact package in Android Studio's SDK Manager. |
Release app bundle failed to strip debug symbols from native libraries |
Flutter checks every release App Bundle with a tool from the Android SDK Command-line Tools, and ends flutter build appbundle with this error when it cannot run that check. The usual cause is that the command-line tools are not installed, which flutter doctor also reports. Install them in Android Studio's SDK Manager and build again. |
"androidApplicationId" is "…", which is not a valid Android application ID. |
Two or more dot-separated parts, each starting with a letter, using only letters, digits and underscores. See Branding. |
"appName" contains a control character |
A tab or line break got into the name. Make it one line. |
More failures, including ones the app shows at run time, are on Troubleshooting.