Turning your local Flutter project into a polished, production-ready Android application requires more than just compiling the code. You must secure it with a cryptographic signature, optimise its size, and package it into the exact formats required by the Google Play Store. A misconfigured build can lead to bloated app sizes, rejected submissions, or even the permanent loss of your ability to update your own application.
This guide walks you through the complete Android release pipeline for a Flutter application. We will generate a secure release keystore, configure your Gradle files to automatically handle signing credentials, and execute the terminal commands needed to output both standalone APKs and Google Play App Bundles. We will also explore essential release practices like Dart code obfuscation, ABI splitting, and version bumping.
Before running any release commands, ensure your application builds successfully in debug mode and that you have finalised your app's package name in the Android manifest. Changing your unique package identifier after you have uploaded your first release to the Play Console is strictly prohibited by Google, so verify it before proceeding.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowApp Bundles (.aab) vs. Standalone APKs
When preparing an Android release, Flutter gives you two primary output formats. Understanding when to use each is crucial for a smooth distribution strategy.
The Android App Bundle (AAB) is Google Play's required publishing format for new apps. An AAB does not install directly onto an Android device. Instead, it contains your compiled code, assets, and native libraries so Google Play can generate optimised APKs for each device's screen density, languages, and CPU architecture. This usually reduces what each user has to download.
An APK (Android Package), on the other hand, is a fully compiled, executable file that can be installed directly onto a phone. You will build APKs when you need to distribute your application outside of the Play Store. This includes side-loading the app onto test devices, sending builds to QA teams via Firebase App Distribution, or deploying internal enterprise software directly to employees.
Managing Versions in pubspec.yaml
Google Play requires every new upload to have a higher version number than the previous one. In Flutter, you control both the user-facing version name and the internal build number directly from your pubspec.yaml file.
name: my_production_app
description: A new Flutter project.
# The format is versionName+versionCode
version: 1.0.2+4
The string before the plus sign (1.0.2) is the versionName. This is the semantic version displayed to your users in the Play Store and inside the app's settings. It has no bearing on Google Play's internal upload logic.
The integer after the plus sign (4) is the versionCode. This is a strictly sequential number used by Android to determine if one build is newer than another. If you upload an App Bundle with version code 4, your next update must be 5 or higher. If you forget to increment this integer, the Google Play Console will reject your upload.
Generating a Release Keystore
Android requires all applications to be digitally signed with a cryptographic certificate before they can be installed. For production, you must create a private keystore. This file proves your identity as the developer and prevents malicious actors from publishing fake updates to your app.
You can generate a keystore using the keytool utility, which is bundled with the Java Development Kit (JDK). Open your terminal and run the following command. On macOS and Linux:
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
If you are using Windows, the command structure is slightly different to accommodate the file system:
keytool -genkey -v -keystore "%USERPROFILE%\upload-keystore.jks" -keyalg RSA -keysize 2048 -validity 10000 -alias upload
The tool will prompt you to create a keystore password, answer several identification questions (like your name and organisation), and finally create a key password. Keep these passwords safe; you will need them in the next step. The -validity 10000 flag ensures your certificate remains valid for roughly 27 years, comfortably exceeding the lifespan of most applications.

The Complete Flutter Guide: Build Android, iOS and Web apps
Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.
Enrol nowLinking the Keystore with key.properties
Hardcoding your keystore passwords directly into your build scripts is a massive security risk, especially if your code is hosted on a platform like GitHub. Instead, Flutter relies on a local properties file to inject these credentials during the build process.
Create a new file named key.properties inside the android/ directory of your Flutter project. Add the following lines, replacing the placeholder values with your actual passwords and the absolute file path to the .jks file you just generated:
storePassword=your_secure_store_password
keyPassword=your_secure_key_password
keyAlias=upload
storeFile=/Users/username/upload-keystore.jks
Crucially, you must prevent this file from being committed to version control. Open your project's root .gitignore file and add the following rules to protect your signing credentials:
# Secure Android signing files
android/key.properties
*.jks
Configure build.gradle.kts for release signing
With your properties file in place, instruct Gradle to read these values and apply them to release builds. Current Flutter projects use the app-level Kotlin DSL file at android/app/build.gradle.kts.
Add these imports at the very top of the file, before the plugins block:
import java.io.FileInputStream
import java.util.Properties
Then put the property-loading declarations after the plugins block and before android { ... }:
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
Next, scroll down to the android { ... } block. You need to define a signingConfigs section and then bind it to the buildTypes release configuration. Update your file to match this structure:
android {
// ... other configurations ...
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}
Older projects may still have android/app/build.gradle; in that case, use the equivalent Groovy syntax rather than pasting Kotlin DSL into that file.
Targeting the Correct Android SDK
The Google Play Store enforces strict API level requirements. Every year, Google mandates that new apps and updates target a recent Android version to ensure compatibility with modern privacy and security features.
In a current project, open android/app/build.gradle.kts and inspect defaultConfig. These defaults come from the Flutter Gradle plug-in, not from values you should hand-edit in local.properties:
defaultConfig {
applicationId = "com.yourcompany.appname"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
If a dependency requires a higher minimum Android version, override minSdk in this Gradle block and document the devices you are dropping. Google Play's annual compatibility rule concerns targetSdk, whereas minSdk decides the oldest device that can install the app. Older projects may still use Groovy syntax in android/app/build.gradle, but the values belong in Gradle either way; do not try to redefine Flutter's SDK constants in local.properties.
Building the App Bundle for Google Play
Once your signing configuration is complete, generating the App Bundle is straightforward. Open your terminal at the root of your Flutter project and execute:
flutter build appbundle
Flutter will compile your Dart code ahead-of-time (AOT) for multiple architectures and package them into a single file. Because this process includes aggressive tree-shaking and compilation, it will take significantly longer than a standard debug build.
When the process finishes, you will find your deployment-ready file at build/app/outputs/bundle/release/app-release.aab. This is the exact file you will drag and drop into the Google Play Console under the "Production" or "Internal Testing" tracks.
Building Standalone APKs and Splitting ABIs
If you need an APK for external distribution, you might be tempted to run a basic flutter build apk --release. However, this command generates a "fat APK". A fat APK contains the compiled binaries for every possible CPU architecture (ARM32, ARM64, and x86_64). While convenient because it runs on any device, it results in a massive file size.
To optimise your standalone APKs, tell Flutter to split the build per Application Binary Interface (ABI):
flutter build apk --split-per-abi
This command outputs three separate, lightweight APKs in the build/app/outputs/flutter-apk/ directory:
app-armeabi-v7a-release.apk(for older 32-bit Android devices)app-arm64-v8a-release.apk(for modern 64-bit Android devices, the most common target)app-x86_64-release.apk(primarily for Android emulators and Chromebooks)
You can now distribute the specific APK that matches your tester's device, saving bandwidth and storage space.
Code Obfuscation and Debug Symbols
Because Flutter compiles Dart into native machine code, reverse engineering your exact source code is already difficult. However, class names, function names, and string literals remain visible in the compiled binary. To further protect your intellectual property, you should obfuscate your code.
Obfuscation scrambles your identifiers into meaningless characters. In Flutter, you must use the --obfuscate flag alongside the --split-debug-info flag. The latter tells Flutter where to save the mapping files needed to translate obfuscated crash logs back into readable code.
flutter build appbundle --obfuscate --split-debug-info=build/app/outputs/symbols
If your application crashes in production, tools like Firebase Crashlytics or the Google Play Console will show unreadable stack traces. You must upload the generated symbol files from the build/app/outputs/symbols directory to these platforms to de-obfuscate the errors and identify the root cause.
Gate debug logs by build mode
A common mistake developers make is leaving print() statements scattered throughout their code. In a release build, these logs are still evaluated and can leak sensitive information (like authentication tokens or API responses) to anyone viewing the device's system logs via logcat.
To keep this debug-only call out of profile and release builds, use Flutter's kDebugMode constant from the foundation package:
import 'package:flutter/foundation.dart';
void secureLog(String message) {
if (kDebugMode) {
debugPrint('DEBUG: $message');
}
}
debugPrint() is not muted automatically in release builds; it is a throttled console writer, not a build-mode gate. Keep it behind a compile-time constant such as kDebugMode, as above, or configure a logging package with an explicit production level. Because kDebugMode is constant, release and profile compilation can remove the guarded debug branch.
Handling Google Play App Signing
When you create a new application in the Google Play Console, Google enables "Play App Signing" by default. This means the keystore you generated earlier is actually treated as an Upload Key.
You sign your App Bundle with your Upload Key, and Google Play verifies it upon upload. Once verified, Google strips your signature and re-signs the application with a highly secure App Signing Key stored on their servers. This is a massive safety net: if you ever lose your upload-keystore.jks file, you can contact Google Developer Support to reset it. If you were managing the final App Signing Key yourself and lost it, you would permanently lose the ability to update your app.
Ensure you back up your .jks file securely, but rest easy knowing that the modern Play Store infrastructure protects your application's long-term upgrade path.
Further reads
Keep going with these related tutorials from this site.
- Flutter: The Complete Guide — the full Flutter learning path on this site
- Flutter App Icons and Splash Screens Setup — learn how to brand your app before publishing
- Flutter Flavors and Environment Config — set up separate Firebase projects for dev and prod
- Flutter DevTools: Inspect Widgets and Debug — profile your app's performance before creating a release build
- Flutter Late Initialisation Error — catch and fix common null errors before your users see them