The Complete Guide to Publishing a Flutter App on Google Play Store
A detailed guide for first-time app publishers. Follow each step at your own pace.
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 — pay once, use forever |
| Credit or debit card | For paying the fee (must support international payments) |
| App signing key (Keystore) | Your app's "digital seal" |
| 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. It may feel like a lot of overhead, but once you've done it once, future updates are much simpler.
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 — much cheaper compared to the Apple Developer Program's annual $99.
- You'll need a credit or debit card that supports international payments.
- Account activation after payment can take up to 48 hours.
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 your app's "digital seal." Only apps signed with your key are recognized as the genuine article.
Here's why it matters: imagine someone creates a fake app with the same name and package ID as yours but with malicious code. Without your signing key, they can't register it as an update to your existing app — which significantly reduces the risk of users being deceived.
Never Lose It
If you lose your Keystore file or its password, you can never update that app again. You'd have to register a completely new app under a new package name, and existing users would have to manually install the new version. Always 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 | Uses the RSA encryption algorithm — the most widely used method today |
-keysize 2048 | Sets the key length to 2048 bits — a good balance of security and performance |
-validity 10000 | Sets the key's validity to 10,000 days (~27 years). Set it long since you'll be maintaining the app for a while |
-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: This project uses the Kotlin DSL format (
build.gradle.kts). 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 means debug builds still work in environments wherekey.propertiesdoesn't exist (e.g., CI servers, another developer's machine).
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 never be changed. Choose carefully. The convention is to use your domain name in reverse. If you don't have a domain,com.github.yourgithubid.appnameworks fine.
After changing the applicationId, also verify the package name in android/app/src/main/AndroidManifest.xml. In recent Flutter versions, updating namespace and applicationId in build.gradle.kts is all that's needed.
6. Building a Release AAB
AAB vs APK
Since August 2021, Google Play requires all new apps to be submitted as AAB (Android App Bundle) format — not APK.
| 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
The build typically takes 1–5 minutes. 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. It's typically 10–50 MB.
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 (cannot be changed to free after setting as paid)
- 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
In the left menu, go to "Setup" > "App signing".
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. Thanks to this two-key structure, if you lose your upload key, you can contact Google to register a new one. That said, it's still best not to lose it in the first place.
7-4. Upload the AAB File
- In the left menu, click "Release" > "Production".
- 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, it's strongly recommended to 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. Promote to Production only after thorough testing.
8. Writing Your Store Listing
Your store listing is everything users see when they find your app on Google Play. The more effort you put in here, the more downloads you'll get.
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
- Displayed at the top of your app's store page.
- 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
- Aspect ratio: 16:9 or 9:16
- Minimum size: 320px, maximum size: 3840px
- 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: Adding descriptive text to your screenshots makes them look much more professional. 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.
- This increases the chance of your app being shown to tablet users.
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".
- You'll see the version you saved earlier. Click "Review release".
- Fix any errors shown (red). Warnings (yellow) don't block publishing, but errors must be resolved.
- Click "Start rollout to Production".
9-3. Review Timeline
- New apps: Typically 3–7 business days. Can take up to 2 weeks in some cases.
- App updates: Typically 1–3 business days.
- 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
Unfortunately, there's no way to recover a Keystore password. If you're using Google Play App Signing, you can contact Google to 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 150 MB
Google Play's AAB file size limit is 150 MB. Ways to reduce Flutter app size:
# Check for unused packages
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 instantly without review |
| Closed testing | Manage testers via email list or Google Groups. Light review process |
| 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. "This app does not meet Google Play's target API level requirements"
Google Play raises the targetSdkVersion requirement every year. Using the latest Flutter version usually satisfies this automatically, 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 = 34 # or whatever the latest required API level is
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
Publishing your first app can feel overwhelming, but it gets much easier the second time around. Follow the steps above one by one and you'll get there.
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.
- Complete the store listing in full.
- Submit for review and wait for approval.
Good luck!