All Posts
September 1, 202618 min read

Google Play Store Deployment: The Complete Guide for Flutter Apps

FlutterAndroidGoogle PlayDeploymentCI/CD
Google Play Store deployment guide for Flutter apps showing signing, release tracks and store listing

Introduction

Shipping your Flutter app to the Google Play Store is a multi-step process that trips up many developers the first time. Signing keys, App Bundles, ProGuard, release tracks, store listings, review policies - each step has specific requirements that must be done correctly or your submission gets rejected.

This guide walks through every step of the full Play Store deployment process: from creating your keystore on day one, to automating releases with CI/CD so future deployments require zero manual intervention.


Step 1: Create Your Release Keystore

A keystore is a cryptographic file that signs your Android APK or App Bundle. This signature proves to Google that the app comes from you. Critical: if you lose your keystore, you cannot update your app on the Play Store. Store it securely.

# Generate a new keystore
keytool -genkey -v \
  -keystore ~/keystores/my-app-release.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias my-app-key

You will be prompted for:

  • Keystore password: choose a strong password, remember it
  • Key alias: a name for this key (e.g., my-app-key)
  • Key password: can be the same as keystore password
  • Your name, organization, city, country

Store the .jks file somewhere secure - a password manager, encrypted cloud storage, or a hardware key vault. Never commit it to git.


Step 2: Configure Signing in Flutter

Reference the keystore from your Gradle build

Create android/key.properties (add this file to .gitignore):

storePassword=your_keystore_password
keyPassword=your_key_password
keyAlias=my-app-key
storeFile=/Users/yourname/keystores/my-app-release.jks

Update android/app/build.gradle

// Load key.properties at the top of the file
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
    // ...

    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            // Enable shrinking and obfuscation
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

Step 3: Configure ProGuard

ProGuard shrinks, optimizes, and obfuscates your release build. Create android/app/proguard-rules.pro:

# Flutter
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }

# Firebase (if using)
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }

# RevenueCat (if using)
-keep class com.revenuecat.purchases.** { *; }

# Gson (if using)
-keepattributes Signature
-keepattributes *Annotation*
-dontwarn sun.misc.**
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

# Your app's model classes (adjust to your package)
-keep class com.yourcompany.yourapp.models.** { *; }

Step 4: Set App Version

In pubspec.yaml, increment your version number before every release:

# Format: major.minor.patch+buildNumber
# Build number must be incremented for every Play Store upload
version: 1.0.0+1

The part before + is the user-visible version name. The number after + is the build number (versionCode) - it must be higher than any previously uploaded build.


Step 5: Build the App Bundle (AAB)

Google Play requires AAB (Android App Bundle) format for new apps since August 2021. AAB lets Google optimize the download size for each device:

# Build release AAB
flutter build appbundle --release

# Output: build/app/outputs/bundle/release/app-release.aab

The AAB is typically 30-50% smaller than an equivalent APK because Google splits it and delivers only what each device needs.

# If you need an APK for testing (not for Play Store)
flutter build apk --release --split-per-abi

# Outputs separate APKs for arm64-v8a, armeabi-v7a, x86_64
# Much smaller than fat APK

Step 6: Set Up Your Google Play Console Account

  1. Go to play.google.com/console
  2. Pay the one-time $25 developer registration fee
  3. Complete identity verification
  4. Create a new app: All apps > Create app
  5. Fill in:
    • App name (what users see on the Store)
    • Default language
    • App or game
    • Free or paid

Step 7: Create the Store Listing

A complete store listing is required before your app can be reviewed. Fill in:

Main Store Listing

  • Short description (80 characters max): your app's core value proposition
  • Full description (4000 characters max): detailed explanation with keywords
  • App icon: 512x512 PNG, no transparency, no rounded corners (Google adds them)
  • Feature graphic: 1024x500 PNG (shown at top of store listing)
  • Screenshots: minimum 2, maximum 8 per device type
    • Phone: minimum 1080x1920 or 1920x1080
    • Tablet: separate set recommended

Content Rating

Complete the content rating questionnaire (IARC). Most business apps get an "Everyone" rating. Games with violence or mature themes get higher ratings that affect discoverability.

Data Safety Section

This is mandatory since 2022 and highly scrutinized. Accurately declare:

  • What data you collect (name, email, device ID, etc.)
  • Why you collect it (app functionality, analytics, etc.)
  • Whether data is shared with third parties
  • Whether users can request data deletion

Inaccurate data safety declarations are a common reason for rejection.


Step 8: Release Tracks

Play Store uses a tiered release system. Use them in order:

Internal Testing (up to 100 testers, instant publish)
        |
Closed Testing / Alpha (defined list of testers)
        |
Open Testing / Beta (anyone can join)
        |
Production (all users, percentage rollout available)

Upload to Internal Testing First

  1. Dashboard > Testing > Internal testing > Create new release
  2. Upload your .aab file
  3. Add release notes
  4. Save > Review release > Start rollout

Internal testing releases go live within minutes. Use this track to share with your team before anything goes to real users.

Uploading a New Release

For every subsequent release:

# Increment version in pubspec.yaml first
# version: 1.0.1+2  (build number must increase)

flutter build appbundle --release
# Upload the new .aab to Play Console

Step 9: Moving to Production

Before promoting to production, complete:

  1. App content review: ensure your app content matches your store listing and content rating
  2. Privacy policy URL: required for all apps that request any permissions
  3. Target API level: must target Android 14 (API 34) or higher as of August 2024
  4. Production release: set rollout percentage (start with 10-20% to catch issues before full rollout)

Staged Rollout

Production release > New release > Set rollout percentage: 10%

Monitor crash rates and ANR (Application Not Responding) rates in Android Vitals. If metrics look good after 24-48 hours, increase to 50%, then 100%.


Step 10: Automating with CI/CD (Fastlane)

For teams shipping weekly or more frequently, manual Play Store uploads are unsustainable. Fastlane automates the entire process:

gem install fastlane
cd android
fastlane init

Create android/fastlane/Fastfile:

default_platform(:android)

platform :android do
  desc "Deploy to Play Store Internal Track"
  lane :internal do
    # Build Flutter AAB
    sh("flutter build appbundle --release")

    # Upload to Play Store internal track
    upload_to_play_store(
      track: 'internal',
      aab: '../build/app/outputs/bundle/release/app-release.aab',
      json_key: 'fastlane/google-play-api-key.json',
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true,
    )
  end

  desc "Promote internal to production"
  lane :promote_to_production do
    upload_to_play_store(
      track: 'internal',
      track_promote_to: 'production',
      json_key: 'fastlane/google-play-api-key.json',
      rollout: '0.1',
    )
  end
end

For the json_key, create a Google Play Android Developer API service account:

  1. Google Play Console > Setup > API access
  2. Link to a Google Cloud project
  3. Create a service account with "Release Manager" permissions
  4. Download the JSON key file

Step 11: GitHub Actions Automation

Full CI/CD pipeline that triggers on every merge to main:

# .github/workflows/android-deploy.yml
name: Deploy Android to Play Store

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.24.0'

      - name: Get dependencies
        run: flutter pub get

      - name: Run tests
        run: flutter test

      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/release.jks

      - name: Create key.properties
        run: |
          cat > android/key.properties << EOF
          storePassword=${{ secrets.KEYSTORE_PASSWORD }}
          keyPassword=${{ secrets.KEY_PASSWORD }}
          keyAlias=${{ secrets.KEY_ALIAS }}
          storeFile=release.jks
          EOF

      - name: Build App Bundle
        run: flutter build appbundle --release

      - name: Upload to Play Store
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
          packageName: com.yourcompany.yourapp
          releaseFiles: build/app/outputs/bundle/release/app-release.aab
          track: internal
          status: completed

Store in GitHub Secrets:

  • KEYSTORE_BASE64: base64 -i release.jks | pbcopy
  • KEYSTORE_PASSWORD, KEY_PASSWORD, KEY_ALIAS
  • GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: the JSON key file contents

Common Rejection Reasons and Fixes

Rejection ReasonFix
Inaccurate data safety declarationAudit every SDK you use and declare all data collection
Missing privacy policyAdd a privacy policy URL in App Content settings
App crashes on launchTest release build on a physical device before submitting
Misleading store listingMatch screenshots to actual app UI
Target API level too lowSet targetSdkVersion 34 in build.gradle
Permissions not justifiedAdd <permission-rationale> or remove unused permissions
Unsafe cryptographyUse Android Keystore system, not custom encryption

Conclusion

Play Store deployment requires attention to detail at every step - keystore security, App Bundle format, ProGuard rules, accurate data safety declarations, and staged rollouts. Getting each step right means faster review times and fewer surprises after launch.

The investment in CI/CD automation (GitHub Actions + Fastlane) pays for itself after the second or third release. Once configured, shipping a new build to internal testers becomes a single git push - and promoting to production is a one-click action in the Play Console.


Need help setting up Android deployment or CI/CD automation for your app? I configure end-to-end Android deployment pipelines including signing, automated testing, Play Store uploads, and staged rollouts. Book a meeting to automate your Android releases.


Written by Moeen Ahmad, Senior Software Engineer working across mobile apps, backend systems, cloud deployments, and AI-powered products. I write about practical engineering, real project lessons, and building software that actually ships.

Share

Interested in working together?

Let's discuss your project and explore how I can help bring it to life.