When to use this release checklist

Use this guide when your Flutter Android app already runs on devices, but your release process is still fragile. Typical symptoms are familiar: one developer machine can produce a bundle while CI fails, the Play Console rejects an upload that looked fine locally, or the team keeps version numbers and keystore paths in chat messages instead of in the project. This article is not an introduction to Flutter. It is a production checklist for teams that already ship features and now need a repeatable Android release path. If you still maintain native upload flows beside Flutter screens, see the earlier Android/Kotlin upload article and then come back to close the release side.

Prerequisites: what you should already have

I assume you already have a Flutter project that can run with flutter run, a working Android app module, and a team that understands the difference between debug and release builds. The new capability in this article is not “how to press Build.” The new capability is operational: make signing reproducible, keep JDK and AGP upgrades from breaking release day, validate the generated AAB before upload, and leave enough build log evidence that the next release is easier than the previous one.

Project structure that keeps release configuration visible

Before touching Gradle, review the release-related files and keep them in stable locations:

my_flutter_app/
  android/
    app/
      build.gradle.kts
    gradle/
      wrapper/
        gradle-wrapper.properties
    gradle.properties
    key.properties
  lib/
  pubspec.yaml
  build/
    app/outputs/

Two rules matter here. First, key.properties must have a known lookup path. It can be injected by CI or replaced by a local template, but the project must document where the file is expected. Second, the keystore itself should not live beside ordinary source files without any protection. In mature setups it is stored in a secret manager, an artifact store, or a secured directory that the CI job downloads during release.

New capability: review the build stack before you review the app

A surprising number of “Flutter release” incidents are actually build stack incidents. Before every release candidate, run these command checks:

flutter doctor -v
java -version
cd android
./gradlew -version

This review answers three questions. Which JDK is actually running Gradle? Which Gradle Wrapper version belongs to the repository? Did an Android Studio upgrade silently move the project onto a newer AGP expectation?

One of the most common log failures still looks like this:

Android Gradle plugin requires Java 17 to run. You are currently using Java 11.

When you see that log, stop changing Dart code. Fix the build runtime first. For most active Flutter Android teams, JDK 17 is the safest release baseline because AGP 8.x expects it. If you are planning an AGP 9 migration, treat it as a separate review item. Flutter now documents a built-in Kotlin migration path for AGP 9+, so you should not combine an IDE upgrade, a plugin migration, and a release submission in one uncontrolled step.

Steps to move signing out of personal habits and into project configuration

Create an upload keystore once, then make the project read it explicitly. A typical command is:

keytool -genkey -v \
  -keystore upload-keystore.jks \
  -keyalg RSA \
  -keysize 2048 \
  -validity 10000 \
  -alias upload

Then define key.properties:

storePassword=replace_me
keyPassword=replace_me
keyAlias=upload
storeFile=../secrets/upload-keystore.jks

And wire that file into android/app/build.gradle.kts:

val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}

android {
    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
        }
    }
}

The important point is not the snippet itself. The important point is that a release configuration must be portable. A teammate or CI runner should be able to reproduce the same signing behavior with the same input secrets. After you change signing configuration, run flutter clean once. Cached builds frequently hide a broken configuration until the worst possible moment.

Steps to make versioning and build commands predictable

Production teams should have one primary release command and one primary version source. In Flutter, that usually starts in pubspec.yaml:

version: 1.6.0+10600

Then build with explicit values:

flutter build appbundle --release --build-name 1.6.0 --build-number 10600

If your project uses flavors, be just as explicit:

flutter build appbundle --release --flavor production --target lib/main_production.dart

This looks simple, but it removes a lot of release ambiguity. The team now knows which artifact is expected, which command produced it, and which version should appear in the Play Console. If one person uploads an APK while another prepares an AAB, your release review is already weak before QA even starts.

Validate the release in layers, not with a single green build

A successful build is necessary, but it is not enough. Validate the release in at least three layers.

First, validate signing:

cd android
./gradlew signingReport

The goal is to confirm that the release variant is using the expected certificate and not quietly falling back to a debug key.

Second, validate the produced artifact. Make sure the expected file exists:

build/app/outputs/bundle/release/app-release.aab

If your team wants an installable smoke test before Play upload, use bundletool to convert the AAB into installable packages and run a test install on a release-like device. That is especially useful when native plugins, resource shrinking, or ABI-related behavior differ from your debug runs.

Third, validate business-critical flows. A real release review should cover at least app start, login, remote configuration, image loading, error reporting initialization, and the most valuable user path in your app. Save the build log, the signing output, and the test-device install log. Those three records are what make the next incident diagnosable instead of mysterious.

If QA, product, or support staff participate in release day, turn that smoke test into a short checklist with named owners. A release review becomes much more useful when you can say which flows were checked, on which build, by which device profile, and what the observed result was. That level of review is what makes future rollbacks or hotfixes faster instead of more political.

Pitfall review: common logs that mean you should stop guessing

The first pitfall is runtime incompatibility between JDK, Gradle, and AGP. If the log says AGP requires Java 17, believe the log and fix the runtime instead of editing Flutter packages.

The second pitfall is an invalid keystore path:

Execution failed for task ':app:validateSigningRelease'
Keystore file '.../upload-keystore.jks' not found

When this appears, review whether storeFile is resolved relative to the expected Gradle root and whether CI downloaded the secret into the path your configuration assumes.

The third pitfall is wrong passwords or mismatched keys:

Keystore was tampered with, or password was incorrect

In practice this often comes from a copied secret with trailing whitespace, an incorrect CI environment value, or confusion between the Play upload key and the app signing key managed by Play App Signing.

The fourth pitfall appears during AGP 9 migration. If your log points at kotlin-android, old DSL types, or plugin build failures, review the host Android Gradle files and plugin state before blaming Flutter itself. Flutter 3.44 introduced a documented migration path, but the review still has to happen project by project.

Review checklist before you upload to Play

  • Review whether the target flavor and environment are clearly declared.
  • Review whether JDK, Gradle Wrapper, AGP, and Flutter versions are captured in the release note.
  • Review whether the keystore source, secret owner, and key.properties contract are documented.
  • Review whether the release command and artifact path are the same across local and CI builds.
  • Review whether the signing output, build log, and smoke-test log are archived.
  • Review whether the latest pitfall from this release was turned into a future preflight step.

This review is what moves a team from “the senior Android person can ship it” to “the project can ship it.”

Next step: turn a stable manual release into CI/CD

Once signing, compatibility, validation, and logs are under control, the next step is straightforward: automate the same path in CI/CD. That means secret injection, a locked release command, versioning rules, smoke tests, and a rollback checklist in one pipeline. At that point, your Flutter Android release process stops being an oral tradition and becomes an engineering system.