FLUTTER · ANDROID · GOOGLE PLAY CONSOLE

From a Finished App to Google Play Testing

A step-by-step path from a debug-only Flutter project to a signed build sitting in Play Console's internal testing track — written from the ground up, including the parts that go wrong.

01

Before you start

You'll need three things in place before touching Gradle:

RequirementNotes
Google Play Developer accountOne-time $25 registration fee, tied to a Google account.
Flutter project, debug-testedRuns fine via flutter run — release signing comes next.
Java (JDK)Needed for keytool. Bundled with Android Studio if you have it installed.
02

Lock in your app identity

Two identifiers matter, and one of them can never change once you publish.

Application ID

In android/app/build.gradle.kts, set a unique reverse-domain identifier:

// android/app/build.gradle.kts
defaultConfig {
    applicationId = "com.yourcompany.yourapp"
    // ...
}
Permanent

Never ship with the default com.example.* placeholder — Google Play locks the applicationId to your listing forever on first upload. If you set it up in Play Console before matching it in your code, your build upload will be rejected with a package name mismatch until they agree.

Version

In pubspec.yaml, the number after the + is your versionCode — it must strictly increase with every upload, forever, even across builds that never went live:

# pubspec.yaml
version: 1.0.0+1
03

Generate a signing key

Every release build needs to be cryptographically signed. Generate an upload key with keytool (bundled with your JDK):

keytool -genkey -v -keystore ../upload-keystore-yourapp.jks \
  -keyalg RSA -keysize 2048 -validity 10000 -alias upload

Run this from inside your project's android/ folder if you want the file to land one level up, in your project root — adjust the ../ to taste. The filename itself is arbitrary; it never has to be called upload-keystore.jks.

What it asks for

  • Keystore password — becomes storePassword
  • Key password — becomes keyPassword (press Enter to reuse the keystore password)
  • Name / org / location fields — cosmetic metadata only, no functional effect
No recovery

Write the passwords into a password manager and back up the .jks file somewhere durable. If you lose either, you generally cannot push further updates to that app listing.

Reuse?

Technically one keystore can sign multiple apps — Play App Signing means it only acts as your upload key, not the key that actually signs what users download. Still, a dedicated key per app keeps a compromised or lost key's blast radius contained to one app.

04

Wire signing into Gradle

Create key.properties

New file at android/key.properties (same folder level as build.gradle.kts, not inside app/):

storePassword=yourStorePassword
keyPassword=yourKeyPassword
keyAlias=upload
storeFile=/absolute/path/to/upload-keystore-yourapp.jks

Add it to .gitignore immediately, alongside the keystore itself:

key.properties
*.jks

Edit build.gradle.kts

Kotlin DSL
// at the very top of android/app/build.gradle.kts
import java.util.Properties
import java.io.FileInputStream

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    // ... existing config ...

    signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String?
            keyPassword = keystoreProperties["keyPassword"] as String?
            storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
            storePassword = keystoreProperties["storePassword"] as String?
        }
    }
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }
}
Gotcha

Flutter's default template ships signingConfig = signingConfigs.getByName("debug") inside buildTypes { release { ... } } — make sure that line gets replaced, not left in alongside the new one.

05

Build the release artifact

Play Console requires an App Bundle (.aab), not a raw APK, for new app uploads:

flutter clean
flutter pub get
flutter build appbundle --release

Output lands at:

build/app/outputs/bundle/release/app-release.aab

Verify the signature

jarsigner -verify -verbose -certs build/app/outputs/bundle/release/app-release.aab

Look for your own certificate details (from the keytool prompts) rather than anything saying "Android Debug".

Optional

A release APK is only needed for manually sideloading onto a device to test — Play Console itself never wants one for a new app. Build with flutter build apk --release if you want to test locally first.

06

Set the launcher icon

The icon you upload to the Play Console listing is separate from your app's actual launcher icon (the one on the home screen) — the second one lives inside android/app/src/main/res/mipmap-*/ and won't update on its own.

Add the flutter_launcher_icons package to pubspec.yaml:

dev_dependencies:
  flutter_launcher_icons: ^0.14.1

flutter_launcher_icons:
  android: true
  ios: false
  image_path: "assets/icon/app_icon.png"
  min_sdk_android: 21

Place your source icon (1024×1024 works well) at that path, then run:

flutter pub get
dart run flutter_launcher_icons
Adaptive icons

For Android's adaptive icon system, you can supply separate foreground/background layers instead of one flat image:

flutter_launcher_icons:
  android: true
  ios: false
  image_path: "assets/icon/app_icon.png"
  adaptive_icon_background: "assets/icon/app_icon_background.png"
  adaptive_icon_foreground: "assets/icon/app_icon_foreground.png"
iOS off but still fails?

If ios: false but the tool still throws a PathNotFoundException hunting for an iOS asset catalog file, your ios/ folder is likely incomplete or missing. Safe to ignore if you're Android-only for now; the Android icon step will have already completed successfully before the error.

Rebuild afterward so the new icon actually ships:

flutter clean
flutter build appbundle --release
07

Prepare store listing assets

AssetSpec
App icon512 × 512 px, 32-bit PNG with alpha
Feature graphic1024 × 500 px, JPG or 24-bit PNG (no alpha) — mandatory
Phone screenshotsMinimum 2, up to 8. At least 4 total and at least 3 in a clean 16:9 or 9:16 ratio are needed to be eligible for store promotion. Short side ≥ 1080px.
Short descriptionMax 80 characters
Full descriptionMax 4000 characters
Privacy policy URLMandatory, even for apps collecting minimal data — a hosted static page is enough
Screenshot ratio trap

Modern phones often shoot screenshots at tall ratios like 19.5:9, which Play Console does not count toward the "16:9 or 9:16" requirement. Place raw screenshots onto a fixed 1080×1920 (portrait) or 1920×1080 (landscape) canvas in an editor — Canva's Play Store templates work well — rather than uploading them straight off-device.

08

Set up the Play Console listing

  1. Create the app entry in Play Console, entering the exact applicationId you locked in earlier.
  2. Fill in Store Listing: title, descriptions, graphics.
  3. Complete the Content Rating questionnaire.
  4. Fill out Data Safety — what your app actually collects and shares. Check this against real behavior, not assumptions.
  5. Set target countries and pricing.
  6. Upload the .aab under a testing track (not Production yet).
09

Set up ADB for on-device debugging

You'll want this the moment a release build behaves differently from debug — which happens more often than you'd expect.

Locate or install adb

It ships with the Android SDK, usually under:

%LOCALAPPDATA%\Android\Sdk\platform-tools\adb.exe

Run it directly by full path, or add the folder to your system PATH so adb works on its own from any terminal.

Enable developer options on the phone

  1. Settings → About phone → tap "Build number" 7 times.
  2. Return to Settings → System → Developer options → toggle USB debugging on.
MIUI / Xiaomi

Developer options live under Settings → Additional settings → Developer options, not under About phone. You may also see a second toggle, USB debugging (Security settings), and an Install via USB toggle — both sometimes require signing into your Mi account with an active internet connection to enable.

Connect and authorize

  1. Plug in a data-capable USB cable.
  2. Set the USB mode notification to "File transfer / MTP" — not "Charging only."
  3. A popup appears on the phone: "Allow USB debugging?" — check "Always allow" and accept.
adb devices
# should list your phone's serial number followed by "device"
Says "unauthorized"?

The authorization popup was missed or dismissed. Unplug and replug the cable to trigger it again, and watch the phone screen this time.

Capture a crash log

adb logcat *:E

Open the app, let it crash, then search the output for a block starting with FATAL EXCEPTION — that's the actual error and stack trace.

No cable handy?

Android 11+ supports wireless debugging: Developer options → Wireless debugging → pair with a code, then adb pair <ip>:<port> followed by adb connect <ip>:<port>, with phone and PC on the same Wi-Fi network.

10

Release to internal testing

Don't go straight to Production. Play Console's Internal Testing track processes near-instantly, needs no review, and lets you install the app through the real Play Store pipeline — the same signing and packaging path your actual users will hit.

  1. Testing → Internal testing → upload your .aab.
  2. Add your own Google account (or a small group) as testers.
  3. Share the opt-in link; install via the Play Store app itself, not a sideload.
  4. Check the Pre-launch report after a short wait — Google runs your build on real hosted devices and surfaces crash traces, screenshots per screen, and accessibility flags automatically.
11

Common pitfalls & fixes

"Your APK or App Bundle needs to have the package name X"

Play Console locked the package name in when the app entry was created. Update applicationId in build.gradle.kts to match exactly, then:

flutter clean
flutter build appbundle --release

Crash: ClassNotFoundException for MainActivity

Happens after renaming your package. Changing applicationId/namespace doesn't move the actual Kotlin file — it's still sitting in the old folder with the old package declaration.

# move the file to match the new package path
android/app/src/main/kotlin/com/yourcompany/yourapp/MainActivity.kt

Then update the line at the top of that file:

package com.yourcompany.yourapp

"Version code X has already been used"

Play Console tracks every uploaded versionCode permanently, even from unpublished test uploads. Bump the number after the + in pubspec.yaml and rebuild — there's no way to reuse or free up a version code.

Screenshots rejected for promotion eligibility

See the ratio trap under store listing assets — resize onto a proper 16:9 / 9:16 canvas rather than uploading raw device screenshots.

12

What happens after you upload

Internal testing builds are available almost immediately to your listed testers — no review wait. Moving to Closed, Open, or Production testing triggers Google's review process, which commonly takes anywhere from a few hours to about a week for a first-time app.

Don't treat review as your testing step. Google's review checks policy compliance, not whether your app actually works — a build can pass review and still crash for every real user if it wasn't verified on a physical device first.