Publishing a Flutter App on Google Play
The steps for putting a Flutter app on Google Play for the first time, in order, from developer account registration to review submission.
1. What You Need to Know Before You Start
Building a Flutter app doesn't mean you can immediately upload it to the store. There are a few things you absolutely must prepare before publishing.
Checklist
| Item | Description |
|---|---|
| Google account | The account you'll use to sign up for Play Console |
| $25 registration fee | One-time cost |
| Credit or debit card | For paying the fee (must support international payments) |
| App signing key (Keystore) | The file holding the key that signs your app |
| Store listing images | Screenshots, icon, feature graphic, etc. |
| Privacy policy URL | A page explaining what personal data your app collects |
Why Are These Required?
Google Play is an app market used by billions of people worldwide. To prevent malicious apps and protect users, Google requires developer verification, app signing, and a review process.
2. Registering a Google Play Console Developer Account
2-1. Access Play Console
Go to the Google Play Console sign-up page in your browser.
Sign in with your Google account. This account will be your developer account going forward, so it's recommended to use a dedicated app management account rather than a personal one.
2-2. Choose Your Account Type
There are two types:
- Individual account: Register as an individual developer. You'll need your name and contact information.
- Organization account: Register under a company or organization name. Requires proof of organization (such as a business registration number) and DUNS number verification, which takes more time.
Note: If you're an independent developer, choose "Individual account." You can switch to an organization account later if needed.
2-3. Enter Developer Information
Fill in the following:
- Developer name: The name displayed publicly in the store. Use your real name or brand name.
- Email address: The email users will see for support inquiries. This is public.
- Phone number: Used for identity verification. Must be a number that can receive SMS.
- Website (optional): Enter your developer website if you have one.
2-4. Identity Verification
Since 2023, Google requires identity verification even for individual accounts. You may need to upload a photo of your ID or complete an address verification process. Allow a few days for this to complete.
2-5. Pay the $25 Registration Fee
- This is a one-time fee. The Apple Developer Program is $99 a year.
- You'll need a credit or debit card that supports international payments.
2-6. Confirm Account Activation
Once payment is complete, you'll gain access to the Play Console dashboard. If you can see the "All apps" page, your account is active.
Note: Rather than uploading your app immediately after registration, it's more practical to finish preparing your build first. Follow the steps below before coming back to upload.
3. Generating an App Signing Key (Keystore)
What Is a Keystore?
A Keystore is a file holding the key that signs your app. Google Play only accepts builds signed with the same key as updates to an existing app.
So someone without your signing key can't publish an update under your package name.
What If You Lose It?
Since August 2021, Play App Signing (7-3) is mandatory for new apps, so the key you create here becomes your upload key. If you lose it, you can request an upload key reset in Play Console. A reset takes time, so keep a backup in a safe place.
3-1. Generate a Keystore with keytool
Open a terminal (or command prompt) and run the following command:
keytool -genkey -v -keystore ~/upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
Here's what each option means:
| Option | Description |
|---|---|
-genkey | Generates a new key pair (public key + private key) |
-v | Verbose output |
-keystore ~/upload-keystore.jks | Path and filename where the key will be saved. You can change this to any location you prefer |
-storetype JKS | Saves in Java KeyStore format |
-keyalg RSA | Generates the key with the RSA algorithm |
-keysize 2048 | Sets the key length to 2048 bits |
-validity 10000 | Sets the key's validity to 10,000 days (~27 years). The Android docs recommend at least 25 years |
-alias upload | An alias for the key, used to reference it later |
3-2. Enter the Required Information
After running the command, you'll be prompted for the following:
Enter keystore password: (type your password — it won't be shown on screen)
Re-enter new password: (type the same password again)
What is your first and last name?
[Unknown]: Jane Doe
What is the name of your organizational unit?
[Unknown]: Development
What is the name of your organization?
[Unknown]: MyCompany
What is the name of your City or Locality?
[Unknown]: New York
What is the name of your State or Province?
[Unknown]: NY
What is the two-letter country code for this unit?
[Unknown]: US
You'll be asked to confirm the information at the end. Type y to proceed.
Note: The name, organization, and other details entered here are not displayed in the store — they're just metadata stored inside the key. Don't stress over them. Just make sure you remember the password.
3-3. Verify the File Was Created
Once the command completes, the .jks file will be at the specified path:
ls -la ~/upload-keystore.jks
If the file exists, you're done. Back it up somewhere safe — cloud storage, a USB drive, and ideally multiple locations.
Warning: Never commit this Keystore file to Git. Add it to
.gitignoreimmediately.
4. Creating the key.properties File
What Is key.properties?
Hardcoding sensitive information like your Keystore password and file path directly into your code is risky — anyone who reads the code would see your password. Instead, store this information in a separate file that's only referenced at build time. That file is key.properties.
4-1. Create the File
Create a key.properties file inside your project's android folder:
# Run from the project root
touch android/key.properties
4-2. Fill In the Contents
Open android/key.properties in a text editor and enter the following:
storePassword=your_keystore_password
keyPassword=your_key_password
keyAlias=upload
storeFile=/Users/yourusername/upload-keystore.jks
| Field | Description |
|---|---|
storePassword | The password for the Keystore file (set in step 3-2) |
keyPassword | The password for the key entry (usually the same as storePassword) |
keyAlias | The alias you specified with the -alias option when generating the key |
storeFile | The absolute path to the Keystore file |
Note: Enter the
storeFilepath accurately. On macOS it'll look like/Users/yourusername/upload-keystore.jks; on Windows, useC:\\Users\\yourusername\\upload-keystore.jks(double backslashes) or forward slashes.
4-3. Add to .gitignore
This file contains passwords, so it must never be committed to Git. Open android/.gitignore and add:
key.properties
To verify:
# Run from the project root
git status
If key.properties doesn't appear in the "Untracked files" list, .gitignore is working correctly.
5. Configuring Signing in build.gradle.kts
Why Do We Need to Modify build.gradle.kts?
A fresh Flutter project is configured to sign with a debug key by default. The debug key is only for testing during development. To submit to the store, you need to sign with the key you just created. The build.gradle.kts file is where you tell the build system which key to use for release builds.
Note: Recent Flutter projects use the Kotlin DSL format (
build.gradle.kts) by default. When searching online, you may find examples using the Groovy format (build.gradle) — the syntax is different, so be careful.
5-1. The Original File
The original android/app/build.gradle.kts looks roughly like this:
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.just_qr"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
applicationId = "com.example.just_qr"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}
5-2. The Updated File
Update it as follows. Changed sections are marked with comments:
import java.util.Properties // [ADDED] Import Properties class
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
// [ADDED] Read key.properties file
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(keystorePropertiesFile.inputStream())
}
android {
namespace = "com.example.just_qr"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
// [ADDED] Define signing configuration
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
defaultConfig {
applicationId = "com.example.just_qr"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// [CHANGED] Switch from debug signing to release signing
signingConfig = signingConfigs.getByName("release")
}
}
}
flutter {
source = "../.."
}
5-3. Detailed Explanation of Changes
1) Importing Properties and reading key.properties
import java.util.Properties
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(keystorePropertiesFile.inputStream())
}
Propertiesis a Java class for reading.propertiesfiles.rootProject.file("key.properties")locates theandroid/key.propertiesfile.- The
if (keystorePropertiesFile.exists())check reads the file only when it exists. But if it's missing, theas Stringcasts insigningConfigsbelow hit null and fail during Gradle configuration, so even debug builds break wherekey.propertiesdoesn't exist (e.g., CI servers, another developer's machine). To build there too, wrap thesigningConfigsblock in the same check.
2) The signingConfigs block
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
create("release")creates a new signing configuration named "release".- Values read from
key.propertiesare mapped to each property. storeFilemust be wrapped in thefile(...)function to convert it to aFileobject.
3) Updating the signing config in buildTypes
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
}
}
- The existing
"debug"reference is changed to"release". - Release builds will now be signed with the Keystore you created.
5-4. Update applicationId (Important)
In the code above, applicationId is set to "com.example.just_qr". Package names starting with com.example cannot be registered on Google Play. You must change it to a unique package name.
applicationId = "com.yourdomain.just_qr"
Examples:
com.janedoe.justqrio.github.yourgithubid.justqr
Warning: Once an
applicationIdis registered on the store, it can't be changed. The convention is to use a domain you own in reverse. If you don't have one,io.github.yourgithubid.appnameis a common choice.
Recent Flutter templates have no package attribute in AndroidManifest.xml; the package name comes from namespace and applicationId in build.gradle.kts. If you change namespace, also update the package declaration and folder location of MainActivity.kt.
6. Building a Release AAB
AAB vs APK
Since August 2021, Google Play requires all new apps to be submitted in AAB (Android App Bundle) format.
| Format | Use Case |
|---|---|
| APK | Direct device installation or testing. Cannot be submitted to the store (for new apps) |
| AAB | For submission to Google Play. Google automatically generates optimized APKs for each user's device |
AAB results in smaller app sizes because Google selects only the resources needed for each specific device when generating the APK.
6-1. Pre-Build Checks
Before building, verify the following:
# Check that Flutter environment is healthy
flutter doctor
# Make sure all packages are installed
flutter pub get
# Check for code errors
flutter analyze
6-2. Run the AAB Build
From your project root directory, run:
flutter build appbundle --release
On success, you'll see:
Built build/app/outputs/bundle/release/app-release.aab
6-3. Locate the Build Output
The generated AAB file is at:
build/app/outputs/bundle/release/app-release.aab
This is the file you'll upload to Google Play Console.
6-4. Troubleshooting Build Failures
If the build fails, check the following:
- Is the path and password in
key.propertiescorrect? - Does the Keystore file actually exist at the specified path?
- Are there any typos in
build.gradle.kts? - Try running
flutter cleanand rebuilding:
flutter clean
flutter pub get
flutter build appbundle --release
6-5. Version Management
App versions are managed in pubspec.yaml:
version: 1.0.0+1
1.0.0is theversionName— the version number shown to users.+1is theversionCode— an integer used internally by the store. It must be higher than the previous value with every update.
When releasing updates:
version: 1.0.1+2 # Bug fix
version: 1.1.0+3 # New feature
version: 2.0.0+4 # Major changes
Warning: Forgetting to increment the
versionCode(the number after+) will cause your upload to be rejected. It must always be greater than the previous submission.
7. Creating Your App and Uploading the AAB in Play Console
7-1. Create the App
- Log in to Google Play Console.
- From the "All apps" page, click "Create app".
- Fill in the following:
- App name: The name displayed in the store (can be changed later)
- Default language: English (or your preferred language)
- App or game: App
- Free or paid: Free (an app published as free can't be changed to paid later)
- Agree to the Developer Program Policies and US export laws.
- Click "Create app".
7-2. Understanding the Dashboard
After creating the app, you'll see the dashboard. The left menu has many sections you'll need to fill in before you can publish. The top of the dashboard shows "Tasks to complete before your app can be published" — work through these one by one.
7-3. App Signing Setup
Go to the App signing settings page in Play Console (its menu location has moved as Play Console has been redesigned).
With Google Play App Signing, Google securely manages your app's signing key. Your key acts as an "upload key," while Google generates a separate "app signing key" to sign the APK that reaches users.
Note: Google Play App Signing is mandatory for new apps. Because the upload key and app signing key are separate, if you lose your upload key you can request a reset in Play Console and register a new one.
7-4. Upload the AAB File
- In the left menu, click "Release" > "Production". On a new personal account, Production is still locked, so start on the "Closed testing" track (7-5).
- Click "Create new release".
- Under the "App bundles" section, upload your AAB file — drag
app-release.aabonto the upload area or use the file picker. - Once uploaded, the version code and version name will appear automatically.
- Write release notes — the "What's new" text shown to users.
Example:
- Initial release
- QR code generation and scanning
- Click "Save". Don't click "Review release" yet — you need to complete the store listing first.
Tip: If it's your first time, upload to the "Internal testing" track first. Internal testing restricts installation to specific email addresses, so any issues won't be exposed to the general public.
7-5. New Personal Accounts Start with a Closed Test
Personal developer accounts created after November 13, 2023 can't publish straight to Production. First run a closed test with at least 12 testers opted in continuously for at least 14 days, then apply for production access from the dashboard. Until then, the Production track stays disabled.
8. Writing Your Store Listing
Your store listing is everything users see when they find your app on Google Play.
8-1. Main Store Listing
In the left menu, go to "Store presence" > "Main store listing".
App name (max 30 characters)
Good: Just QR - Fast QR Code Reader
Bad: QRCodeReaderScannerGeneratorFreeBest (keyword stuffing)
Warning: Excessive keyword repetition in the app name or description may violate Google's policies and result in rejection.
Short description (max 80 characters)
A one-line description shown beneath the app name:
Example: Scan and create QR codes quickly and easily
Full description (max 4,000 characters)
Describe your app's features in detail. Naturally incorporate key search terms for App Store Optimization (ASO).
Example:
Just QR is a clean app for scanning and generating QR codes in one place.
Key features:
- Instantly scan QR codes with your camera
- Generate QR codes for URLs, text, contacts, and more
- Save generated QR codes to your gallery
- Clean and intuitive interface
...
8-2. Graphic Assets (Images)
Prepare the following images for your store listing:
App icon (required)
- Size: 512 × 512 pixels
- Format: PNG (32-bit, alpha allowed)
- Use the same design as your Flutter app icon at high resolution.
Feature graphic (required)
- Size: 1024 × 500 pixels
- Used as a banner in several places on Google Play.
- Create a banner image that showcases your app's core function.
- Free tools like Figma or Canva work well for this.
Screenshots (required)
- Minimum 2, maximum 8
- Phone screenshots: at least 2 required
- Format: JPEG or 24-bit PNG (no alpha)
- Each side 320px to 3840px, and the long side can't be more than twice the short side
- Use real app screenshots or mockups with device frames.
- Pick screens that best highlight your app's key features.
How to take screenshots:
# Run the Flutter app and take a screenshot in the emulator
flutter run
# Or capture from a physical device
# Android: Press Power + Volume Down simultaneously
Tip: To add descriptive text to your screenshots, search for "app store screenshot template" in Figma or Canva for free templates.
Tablet screenshots (optional)
- If your app works well on tablets, add tablet screenshots too.
8-3. Category and Tags
Category
- Choose the category that best fits your app.
- For a QR code app, "Tools" is appropriate.
Tags
- Select relevant tags from the list Google provides.
8-4. Contact Details
- Email address (required): For user support inquiries. This is public.
- Phone number (optional): Public if provided.
- Website (optional): Your app's website, if you have one.
8-5. Privacy Policy
Google Play requires a privacy policy URL for all apps. Even if your app collects no personal data at all, you still need a page that explicitly states that.
Ways to create a privacy policy page:
-
GitHub Pages (free, recommended)
- Create a
privacy-policy.mdfile in a GitHub repo and deploy it with GitHub Pages.
- Create a
-
Free generator tools
- Use tools like App Privacy Policy Generator.
-
Google Sites (free)
- Create a simple page on Google Sites.
Your privacy policy must cover:
- What personal data the app collects (or state explicitly that it collects none)
- How collected data is used
- How long data is retained and how it's deleted
- Whether data is shared with third parties
- Contact information for inquiries
Simple privacy policy example:
# Privacy Policy
The Just QR app ("the App") does not collect any personal information.
## Information Collected
This app does not collect, store, or transmit any personal data.
## Camera Permission
This app uses the camera permission solely for scanning QR codes.
Images captured via the camera are processed entirely on-device
and are never transmitted to external servers.
## Contact
For inquiries about this privacy policy, please contact:
Email: your-email@example.com
Effective date: January 1, 2024
8-6. App Content Settings
In the left menu, go to "Policy" > "App content" and complete the following declarations:
Privacy policy: Enter the URL you created above.
Ads: Indicate whether your app contains ads.
App access: Indicate whether all app features are freely accessible or whether a login is required.
Content ratings: Complete the questionnaire to receive an automatic IARC rating. You'll be asked about violence, sexual content, etc., and an age rating will be assigned based on your answers.
Target audience: Select the intended age range for your app. Additional requirements apply if your app targets children under 13.
Data safety: Declare what types of data your app collects and shares. You must complete this section even if your app collects no data.
Note: Every item in the "App content" section must be completed. Missing even one will prevent you from submitting for review.
8-7. Country / Region Settings
Under "Release" > "Production" > "Countries / regions", select where you want your app to be available.
- To release in all countries, use "Add countries/regions" and select all.
- To release in a specific country only, select just that country.
9. Submitting for Review
9-1. Pre-Submission Checklist
Before submitting for review, confirm all of the following:
- Does
applicationIdNOT start withcom.example? - Is the version code and version name correct?
- Is the main store listing complete (name, description, screenshots, etc.)?
- Is the privacy policy URL valid and accessible?
- Is the App content section fully filled in?
- Is the content rating questionnaire complete?
- Is the data safety section complete?
- Are target countries selected?
- Is the AAB file successfully uploaded?
9-2. Submit for Review
- Check the Play Console dashboard. "Tasks to complete before publishing" should show 0.
- Go to "Release" > "Production".
- Open the version you saved and go to the review step.
- Fix any errors shown. Warnings don't block publishing, but errors must be resolved before you can continue.
- Save the version, then send the changes for review from Publishing overview. Button names have changed as Play Console has been updated, so follow what the screen shows.
9-3. Review Timeline
- Google says that for certain developer accounts it reviews more thoroughly, which can take up to 7 days, or longer in exceptional cases. When that applies, the expected timeframe is shown on your dashboard.
- While under review, the app status will show as "In review."
- You'll receive an email notification when the review is complete.
9-4. Common Reasons for Rejection
- Metadata policy violation: Keyword stuffing in the app name or description
- App not functional: App crashes or core features don't work
- Privacy policy missing or inconsistent: Data collection practices don't match what the privacy policy states
- Excessive permission requests: Requesting permissions unrelated to app functionality (e.g., a calculator requesting camera access)
- Intellectual property violation: Unauthorized use of another brand's logo or name
- Insufficient functionality: App is a simple website wrapper or has very little to offer
9-5. What to Do If Your App Is Rejected
- Read the rejection reason carefully. It will be in Play Console and in the notification email.
- Fix the issue.
- Build a new AAB. (Remember to increment
versionCode!) - Resubmit.
- If the rejection reason is unclear, you can use Play Console's appeal feature.
10. Common Mistakes / FAQ
Q1. I submitted with "com.example" still in the package name
Package names starting with com.example are rejected at the upload stage in Play Console. You need to change both applicationId and namespace in build.gradle.kts. Also update the directory structure under android/app/src/main/kotlin/ to match the new package name.
Q2. I forgot my Keystore password
There's no way to recover a Keystore password. If you're using Google Play App Signing, you can request an upload key reset in Play Console and register a new upload key. Otherwise, the app can no longer be updated.
Q3. My upload is rejected because I didn't increment versionCode
Increment the number after + in the version field in pubspec.yaml:
# Before
version: 1.0.0+1
# After
version: 1.0.1+2
Q4. My app is over 200 MB
Google Play limits an AAB's base module to 200 MB of compressed download size. Ways to reduce Flutter app size:
# List dependencies (remove unused packages from pubspec.yaml)
flutter pub deps
# Remove unnecessary assets (images, fonts, etc.)
# Build with obfuscation and optimization
flutter build appbundle --release --obfuscate --split-debug-info=build/debug-info
--obfuscate makes the code harder to reverse-engineer. --split-debug-info separates debug info into a separate file, reducing app size.
Q5. I'm seeing a warning about 64-bit support
Google Play only allows apps that support 64-bit. Flutter builds for both 32-bit and 64-bit by default, so this usually isn't an issue. If you do see a warning, check the ndk configuration in build.gradle.kts.
Q6. What is the Internal Testing track?
Play Console has several testing tracks:
| Track | Description |
|---|---|
| Internal testing | Up to 100 testers, invite-only via email. Deploys without review |
| Closed testing | Manage testers via email list or Google Groups. Reviewed. New personal accounts meet the 12-tester, 14-day requirement here |
| Open testing | Public beta — anyone can join. Review required |
| Production | Live to all users. Full review |
For first-timers, it's safest to go Internal testing → Closed/Open testing → Production step by step.
Q7. I'm getting a target API level requirement error
Google Play raises the targetSdk requirement every year. Starting August 31, 2026, new apps and updates must target API 36 (Android 16) or higher. Using the latest Flutter version usually satisfies this, but if you're on an older version of Flutter, you may need to upgrade.
# Upgrade Flutter
flutter upgrade
# If that doesn't work, set it directly in build.gradle.kts
# Inside the defaultConfig block:
targetSdk = 36 # the API level Google Play requires
Q8. What's the difference between build.gradle and build.gradle.kts?
When searching online, you'll find examples in two formats:
build.gradle: Groovy-based (older format)build.gradle.kts: Kotlin DSL-based (newer format)
Recent Flutter projects use build.gradle.kts by default. The syntax differs between the two, so copying Groovy examples directly into build.gradle.kts will cause errors. Key differences:
// Groovy (build.gradle)
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
storeFile file(keystoreProperties['storeFile'])
}
}
// Kotlin DSL (build.gradle.kts)
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
}
}
Q9. I want to update my app after it's live
- Make your code changes.
- Increment the version in
pubspec.yaml(versionCode is mandatory). - Build a new AAB with
flutter build appbundle --release. - Create a new release in Play Console and upload the new AAB.
- Submit for review.
Q10. Can I change a free app to paid?
No. Once an app is published as free, it cannot be changed to paid. The reverse — changing paid to free — is allowed. If you're considering monetization, either start as paid from the beginning or use in-app purchases (IAP) instead.
Wrap-Up
Key summary:
- Register a Play Console developer account ($25 one-time fee).
- Generate a Keystore and keep it safe.
- Configure
key.propertiesandbuild.gradle.kts. - Build the AAB with
flutter build appbundle --release. - Create your app in Play Console and upload the AAB. New personal accounts run a closed test (12 testers, 14 days) before applying for production access.
- Complete the store listing in full.
- Submit for review and wait for approval.