If you’re prepping a Flutter app for Play Store release on Windows, you need a signed Android App Bundle (.aab), not the debug build Flutter gives you out of the box. This post walks through generating a production keystore, wiring it into your Gradle config, and building the bundle. It covers both build.gradle (Groovy) and build.gradle.kts (Kotlin DSL). Since Flutter projects created more recently default to Kotlin DSL, the syntax trips people up.
Step 1: Generate a keystore
You need Java installed for this (it ships with Android Studio). Open Command Prompt and run:
keytool -genkey -v -keystore C:keystoreupload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
keytool will prompt you interactively for a few things. Two of them matter a lot for the next step:
Enter keystore password:
Re-enter new password:
...
Enter key password for <upload>
(RETURN if same as keystore password):
Whatever you type at “Enter keystore password” becomes your storePassword. Whatever you type at “Enter key password” becomes your keyPassword. If you just hit Enter at the second prompt (the default, and what most people do), your keyPassword is identical to your storePassword.
There’s no way to recover these later. keytool doesn’t expose stored passwords. If you lose them after publishing to Play Store, you’re stuck unless you go through Google’s key upgrade process for Play App Signing. Put both passwords in a password manager the moment you generate the keystore, not after.
Also, keep the keystore path short. Windows has path length limits, and deeply nested project folders, combined with a long keystore path, can cause errors that appear unrelated to the actual problem.
Step 2: Create key.properties
In your Flutter project root, create android/key.properties:
storePassword=<value from "Enter keystore password">
keyPassword=<value from "Enter key password">
keyAlias=upload
storeFile=C:\keystore\upload-keystore.jks
Use double backslashes for the Windows path, or forward slashes. Both parse fine.
Add this file to .gitignore immediately. It should never end up in version control.
Step 3: Wire it into Gradle
This is where Groovy and Kotlin DSL diverge.
If you have android/app/build.gradle (Groovy)
Add this near the top of the file, before the android { } block:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
Then inside android { }:
android {
...
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
If you have android/app/build.gradle.kts (Kotlin DSL)
At the top of the file, before android { }:
import java.util.Properties
import java.io.FileInputStream
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
Then inside android { }, alongside your existing compileSdk, defaultConfig, etc.:
android {
// ... existing config
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
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
A few things that catch people switching from Groovy:
- Signing configs are declared with
create("release")instead of justrelease { }. - Every property assignment needs an
=. Kotlin is statically typed; Groovy’s dynamic property setters don’t apply here. - Boolean properties get an
isprefix:minifyEnabled truebecomesisMinifyEnabled = true. keystoreProperties["key"]returnsAny?, so you need explicit casts likeas String?to satisfy the compiler.
Step 4: Build the bundle
Same command regardless of which Gradle syntax you’re using:
flutter clean
flutter pub get
flutter build appbundle --release
Output lands at:
buildappoutputsbundlereleaseapp-release.aab
Upload that file to the Google Play Console.
Windows-specific gotchas
keytoolnot found: add Java’sbinfolder to your PATH. If you’re using Android Studio’s bundled JBR, it’s usually atC:Program FilesAndroidAndroid Studiojbrbin.- Keystore not found errors: almost always a backslash escaping issue in
key.properties. Double-check\vs . - Duplicate version code on upload: bump the version in
pubspec.yaml(e.g.1.0.1+2) before every release build. Play Console rejects re-uploads with the same version code.
That covers the full path from keystore generation to a signed .aab ready for Play Store, on both Gradle syntaxes.





