MMashDiv

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.

9 min readUpdated 26 September 2026mobile, android, signing, keystore, aab, apk, google-play, versioning

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
  • androidApplicationId set in assets/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.app

Flutter 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.

  1. 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 upload

    It 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.

  2. Create android/key.properties. It sits in the android/ folder, next to settings.gradle.kts, and not in android/app/. It needs exactly these four keys:

    storeFile=/Users/you/upload-keystore.jks
    storePassword=your-keystore-password
    keyAlias=upload
    keyPassword=your-key-password

    Use an absolute path for storeFile. A relative path is resolved from android/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 .properties file starts an escape.

  3. 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
  4. 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.apk

A 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+10

The 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=14

Google 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=true

This 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

  1. 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:// or ws:// address ([backend-not-encrypted]), which a release build refuses on its first screen.

    dart run tool/release_preflight.dart --explain

    It 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
  2. Raise the build number above your last upload, then build the bundle:

    flutter build appbundle --release
  3. Create the app in Play Console under your organisation's account, then upload app-release.aab to 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.

  4. Install it from the testing track on a real phone and sign in with a real account before you promote anything.

  5. 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.