Start with the signing key, not the build

Android requires every release to be signed with a keystore, and the Play Console keeps the signing certificate it first sees. Losing that keystore means you cannot update your own app, so treat it as a production secret rather than a project file.

Generate the upload keystore outside the repository, record the passwords in a password manager, and keep an offline backup. Never commit a keystore or its passwords to git.

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

Configure signing in the Flutter project

Declare the signing configuration in android/key.properties and reference it from the app module. The properties file stays out of version control; only the Gradle wiring that reads it is committed.

storePassword=<password>
keyPassword=<password>
keyAlias=upload
storeFile=/absolute/path/to/upload-keystore.jks
Point storeFile at an absolute path outside the project, or use an environment variable, so a fresh CI checkout does not silently fall back to debug signing.

Build an app bundle, not an APK

Google Play distributes Android App Bundles and generates optimised APKs per device configuration. Building a universal APK still works for sideloading, but it is no longer the Play distribution format.

flutter clean
flutter pub get
flutter build appbundle --release \
  --dart-define=ENVIRONMENT=production
  • Run flutter clean before the release build
  • Set environment values with --dart-define rather than committing a production .env
  • Confirm the bundle reports the release signing certificate, not debug
  • Test the bundle locally with bundletool before uploading

Play Console setup order

The Play Console has a strict order of operations, and hitting them out of order is the usual cause of a rejected first submission.

  • Create the app and complete the store listing
  • Upload the app bundle to a closed testing track first
  • Complete the Data safety form
  • Complete content rating questionnaires
  • Set target audience and privacy policy
  • Only then request production release

Data safety and permissions must match the app

Play compares the Data safety declaration with the permissions and SDKs your app actually declares. A mismatch between the form and the binary is one of the most frequent causes of rejection.

Every declared permission needs a matching user-facing explanation. If the app collects location for delivery tracking, the form has to say so and the app has to justify it at runtime.

Closed testing before production

Push the bundle to an internal or closed testing track, install it on real devices, and verify the release build rather than the debug build. Release mode behaves differently: obfuscation, tree shaking and AOT compilation can surface problems the debug profile never shows.

Check crash reporting on the test track before opening production. A release-only crash that reaches production will be painful to unwind once users have it installed.

After the release

Publishing is not the end of the release. The first hours are where staged rollouts earn their keep.

  • Start with a staged rollout and increase it as crash rates stay flat
  • Watch Android vitals for ANRs and crash-free sessions
  • Verify deep links, notifications and payments on the store build
  • Keep the previous app bundle available for rollback planning
  • Tag the release in version control so the shipped code is identifiable

Common rejections worth pre-empting

Most first submissions are rejected for predictable reasons rather than code quality issues.

  • Data safety form inconsistent with declared permissions
  • Missing or unusable privacy policy URL
  • Store screenshots that do not reflect the current interface
  • App crashes on the reviewer\u2019s device in release mode
  • Families or finance categories without the required declarations