Build & Release #
The app compilation process for the production (release) stage has very fundamental architectural differences compared to the compilation process during the development/debugging stage. In release compilation, the Dart AOT (Ahead-of-Time) compiler translates all your code directly into native machine binary instructions for maximum execution performance, performs dead code removal (tree shaking), and turns off all debugging ports and assertions.
For the target mobile operating system to be willing to install your production app, the generated binary files (APK/AAB on Android and IPA on iOS) must be cryptographically signed using legitimate digital certificates owned by the developer. This article thoroughly unpacks the signing procedures, optimal release build creation, and app launch management to the Google Play Store and Apple App Store.
Version Numbering Lifecycle (Semantic Versioning) #
Before starting release compilation, you must determine the correct release version in your project’s pubspec.yaml file. Flutter adopts the Semantic Versioning (SemVer) standard to manage this release numbering cycle.
The version writing structure in pubspec.yaml is set on the version parameter line:
version: 2.3.1+45
# │ │ │ └── Build Number (versionCode on Android, CFBundleVersion on iOS)
# │ │ └──── Patch Version (Bug fixes, backward-compatible)
# │ └────── Minor Version (New features, backward-compatible)
# └────────── Major Version (Major changes incompatible with previous versions)
Basic release version management rules:
- Major (2.x.x): Incremented when major architecture overhauls (breaking changes), massive UI redesigns, or data structure changes occur that make the new version unable to directly update the old version.
- Minor (x.3.x): Incremented when you release new functional features compatible with previous versions.
- Patch (x.x.1): Incremented when you release small bug fixes (hotfixes) that don’t introduce new features.
- Build Number (+45): A single integer representing your build compilation iteration. The build number must always be consistently incremented every time you upload a new binary file to the app store, even if the version number doesn’t change. App stores reject new binaries with the same build number as binaries already in their consoles.
Android: Signing & Build Process #
The Android operating system requires every APK or AAB installation file to be signed using a release digital certificate stored in a secure file called a Keystore.
Step 1: Creating a New Keystore File #
Open the terminal console and use the keytool utility built into the Java SDK to create a new signing key:
# Create a secure directory outside your git project folder
mkdir ~/keystore
# Create the release keystore file
keytool -genkey -v \
-keystore ~/keystore/toko-kita-release.jks \
-storetype JKS \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias upload
In this process, you must enter the keystore password and complete the developer identity data. Store this password in a safe place (e.g., in the team’s password manager), because if the keystore file or this password is lost, you’ll never be able to update your app on the Google Play Store again forever.
Step 2: Creating the key.properties File Safely #
Create a new file named key.properties in your project’s android/ directory. This file stores the keystore location details and passwords locally. Never upload this file to Git repositories.
# android/key.properties (IMPORTANT: add this file to .gitignore)
storePassword=ourKeystorePassword
keyPassword=ourKeyPassword
keyAlias=upload
storeFile=/Users/our_username/keystore/toko-kita-release.jks
Step 3: Connecting the Signing Config to Gradle #
Modify the android/app/build.gradle configuration file to automatically read the signing properties file and apply it to release compilation:
// android/app/build.gradle
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 {
// Apply the release digital signing configuration
signingConfig signingConfigs.release
// R8/Proguard minification configuration
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
Step 4: Running the Android Build Command #
- Android App Bundle (AAB) - Play Store Recommendation: This format must be used if you’re launching the app to the Google Play Store. Google Play splits the AAB into small APKs tailored to user device CPU architectures and screen resolutions when downloaded.
flutter build appbundle --release --dart-define-from-file=.dart_define/production.json # Output: build/app/outputs/bundle/release/app-release.aab - Split APK - For Independent Distribution: If you want to distribute APK files directly (e.g., through internal company websites or sent manually), run the per-ABI split command to shrink APK sizes per CPU architecture:
flutter build apk --release --split-per-abi --dart-define-from-file=.dart_define/production.json # The APK output will be divided into: # 1. build/app/outputs/flutter-apk/app-arm64-v8a-release.apk # 2. build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
iOS: Signing & Build Process #
The iOS binary signing process uses the Apple Developer Portal ecosystem which requires a paid Apple Developer account ($99/year).
Prerequisites in the Apple Developer Portal #
- Distribution Certificate: An encrypted p12 digital certificate proving your identity as an official Apple developer.
- App ID & Bundle ID: Your app’s unique identifier registered in the Apple portal (e.g.,
com.unisbadri.tokokita). - Provisioning Profile (App Store Distribution): An authorization file binding your digital certificate with the app’s App ID to be allowed for installation on non-developer devices.
Configuring Xcode Signing #
Open the ios folder using Xcode:
- Select the Runner target in the left panel.
- Go to the Signing & Capabilities tab.
- Select your Apple Developer team in the Team drop-down menu.
- On local developer computers, you can check the Automatically manage signing option for easy automatic test profile registration. However, for release build automation or CI/CD servers, manual profile configuration (Manual Signing) is recommended.
Building the iOS IPA #
Run the initial compilation to produce the Xcode Archive file (.xcarchive):
flutter build ios --release --dart-define-from-file=.dart_define/production.json
After the Dart compilation process finishes, open Xcode to export the archive file into a release IPA binary, or you can use Flutter’s unified CLI command by including an export options file:
flutter build ipa --release \
--dart-define-from-file=.dart_define/production.json \
--export-options-plist=ios/ExportOptions.plist
# Output: build/ios/ipa/toko_kita.ipa
The ExportOptions.plist file is a standard iOS export configuration XML file. Here’s an example of the file contents:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>AB12345XYZ</string> <!-- Your Apple Developer Team ID -->
<key>uploadBitcode</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
Release Compilation and Distribution Workflow #
Overall, the workflow from version writing to uploading release binaries to each app store console can be illustrated in the following chart:
flowchart TD
Start(["Start the Release Process"]) --> VersionCheck["Update the Version & Build Number in pubspec.yaml"]
VersionCheck --> SelectPlatform{"Choose the Target Platform"}
SelectPlatform -- Android --> SignAndroid["Android Signing:<br/>1. Read key.properties<br/>2. Load the Keystore (.jks)"]
SignAndroid --> BuildAAB["Compile the AAB:<br/>flutter build appbundle --release"]
BuildAAB --> ValidateAAB["Validate the AAB via bundletool"]
ValidateAAB --> UploadGoogle["Upload to the Google Play Console<br/>(Internal / Closed / Production)"]
SelectPlatform -- iOS --> SigniOS["iOS Signing:<br/>1. Match the Team & Bundle ID<br/>2. Load the Certificate & Provisioning Profile"]
SigniOS --> BuildIPA["Compile the IPA:<br/>flutter build ipa --release"]
BuildIPA --> UploadApple["Upload to App Store Connect<br/>(TestFlight / App Store Review)"]
UploadGoogle --> End(["The App is Ready to Distribute"])
UploadApple --> EndDistribution and Release to the Google Play Store #
After producing the signed AAB file, you can go to the Google Play Console (play.google.com/console) to start the distribution process.
1. Determining Release Test Tracks #
The Google Play Console provides four main release tracks for testing app stability before broad publication:
- Internal Testing: The fastest track. Releases can be directly enjoyed by a maximum of 100 invited internal testers without needing to go through strict Google review processes. Perfect for daily release builds (daily builds).
- Closed Testing (Alpha/Beta): Invites medium-scale tester groups (up to 2000 people) using email links or Google Groups. Suitable for pre-release testing by external teams.
- Open Testing: A public beta release where anyone can register through the Play Store page to test your app’s beta version.
- Production: The official release downloadable by all general users in the selected regions/countries.
2. Staged Rollout Strategy #
When launching major updates in the Production track, avoid releasing them directly $100%$ at once. This is very risky if there are hidden bugs that slipped past the QA phase.
Use the Staged Rollout feature to publish releases gradually:
$$\text{Rollout Stages} = 1% \rightarrow 5% \rightarrow 10% \rightarrow 20% \rightarrow 50% \rightarrow 100%$$
Monitor the error report graphs in the Google Play Console or Firebase Crashlytics in real-time at every percentage stage. If an error spike (crash rate) is detected at the $5%$ percentage, you can immediately halt the release, fix the bug, then launch the fix release without affecting the remaining $95%$ of your user base.
Distribution and Release to the Apple App Store #
For the iOS platform, you use App Store Connect (appstoreconnect.apple.com) to distribute your IPA file.
1. Distribution via TestFlight #
TestFlight is Apple’s official platform for testing iOS apps before App Store release.
- Internal Testers: A maximum of 100 team members (registered in App Store Connect). Release build files can be installed instantly.
- External Testers: A maximum of 10,000 public testers through open invitation links or emails. These builds require a short review from Apple (usually taking less than 24 hours) before they can be tested.
2. Preparing Data for Apple Reviewers #
Apple’s app review process is famously very strict. Many apps are rejected because of trivial issues that can actually be avoided. Here are tactics to smooth the review process:
- Include a Valid Demo Account: If your app requires login, include valid testing account (demo account) usernames and passwords in the Notes for Reviewer section. Make sure that account’s transaction data is filled in and not empty.
- Explain Permissions: If your app accesses background locations, cameras, or Bluetooth, explain in depth why the app needs those permissions and how to try them in the app.
- Include a Demo Video: If your feature workflows involve interactions with external physical devices (like IoT device connections via Bluetooth), record a short demonstration video of using that device and upload the video link to help Apple reviewers understand how the app works.
Comprehensive Release Readiness Checklist #
Before tapping the final release button, make sure you verify all the following components:
1. Code Analysis & Quality #
- The
flutter analyzecommand runs clean without warnings or errors. - All unit tests, widget tests, and integration tests pass without failures.
- The version name and build number (version code) have been correctly incremented in
pubspec.yaml. - The CHANGELOG file has been updated to record the new feature and bug fix lists.
2. Build & Environment Configuration #
- Compilation is run using the
--releaseflag explicitly. - The
--dart-define-from-fileconfiguration file uses a valid production release profile file. - The native digital signing certificates used match the release account (not debug certificates).
- The app has been test-run directly on release physical devices (not emulators) and performs smoothly.
3. App Store Metadata & Legal #
- All required screenshot image sizes have been updated to match the latest app appearance.
- Short descriptions, full descriptions, and release notes have been translated into the supported languages.
- The privacy policy URL link is active and publicly accessible.
- The content rating questionnaire has been filled out honestly and accurately.
Summary #
- Semantic Versioning: Manage release numbering with the
major.minor.patch+buildNumberstructure. The build number must always increase on every new binary upload.- Android Signing: Create Keystore files securely using
keytool, store them outside Git repositories, and connect the configuration locally viakey.properties. Use the App Bundle (.aab) format for Play Store uploads.- iOS Signing: Leverage legitimate Apple Distribution Certificates and App Store Provisioning Profiles, and configure the IPA export process using the
ExportOptions.plistfile.- Play Store Rollout: Apply the Staged Rollout method gradually ($1% \rightarrow 5% \rightarrow 10% \rightarrow 100%$) to mitigate post-release mass crash risks.
- App Store Review: Avoid app rejections by Apple by including active trial accounts, permission need explanations, and valid privacy policy links in the review information section.