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.
Before you start
You'll need three things in place before touching Gradle:
| Requirement | Notes |
|---|---|
| Google Play Developer account | One-time $25 registration fee, tied to a Google account. |
| Flutter project, debug-tested | Runs fine via flutter run — release signing comes next. |
| Java (JDK) | Needed for keytool. Bundled with Android Studio if you have it installed. |
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"
// ...
}
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
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
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.
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.
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
// 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")
}
}
}
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.
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".
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.
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
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"
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
Prepare store listing assets
| Asset | Spec |
|---|---|
| App icon | 512 × 512 px, 32-bit PNG with alpha |
| Feature graphic | 1024 × 500 px, JPG or 24-bit PNG (no alpha) — mandatory |
| Phone screenshots | Minimum 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 description | Max 80 characters |
| Full description | Max 4000 characters |
| Privacy policy URL | Mandatory, even for apps collecting minimal data — a hosted static page is enough |
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.
Set up the Play Console listing
- Create the app entry in Play Console, entering the exact
applicationIdyou locked in earlier. - Fill in Store Listing: title, descriptions, graphics.
- Complete the Content Rating questionnaire.
- Fill out Data Safety — what your app actually collects and shares. Check this against real behavior, not assumptions.
- Set target countries and pricing.
- Upload the
.aabunder a testing track (not Production yet).
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
- Settings → About phone → tap "Build number" 7 times.
- Return to Settings → System → Developer options → toggle USB debugging on.
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
- Plug in a data-capable USB cable.
- Set the USB mode notification to "File transfer / MTP" — not "Charging only."
- 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"
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.
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.
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.
- Testing → Internal testing → upload your
.aab. - Add your own Google account (or a small group) as testers.
- Share the opt-in link; install via the Play Store app itself, not a sideload.
- 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.
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.
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.