Build and release
Getting a build out of the machine and into a store: build modes, flavours, configuration, signing, artefacts, and the checks worth automating.
Contents
- Build modes
- Versioning
- Configuration
- Flavours
- Android signing
- iOS signing
- Build artefacts
- Obfuscation and symbols
- Size
- CI
- Release checklist
Build modes
| Mode | Compilation | Use |
|---|---|---|
| Debug | JIT, assertions on, hot reload | Development only |
| Profile | AOT, tracing kept | Performance measurement |
| Release | AOT, tracing stripped, minified | What you ship |
flutter run --debug
flutter run --profile
flutter build apk --release
Never benchmark or demo from a debug build — see DevTools and performance.
Versioning
pubspec.yaml drives both platforms:
version: 1.4.2+37
The part before + is the user-visible version (versionName on Android,
CFBundleShortVersionString on iOS); after it is the build number
(versionCode, CFBundleVersion). Override at build time in CI:
flutter build appbundle --build-name=1.4.2 --build-number=37
Both stores reject a build number that is not higher than the last upload, so derive it from something monotonic — the CI run number works well.
Configuration
Do not compile secrets into the app. Anything in the binary is extractable, so API keys for third-party services belong behind your own backend.
For non-secret, per-environment configuration, use compile-time variables:
flutter build apk --dart-define=API_BASE_URL=https://api.example.com \
--dart-define=ENABLE_ANALYTICS=true
class AppConfig {
static const apiBaseUrl =
String.fromEnvironment('API_BASE_URL', defaultValue: 'http://localhost:8080');
static const enableAnalytics =
bool.fromEnvironment('ENABLE_ANALYTICS');
}
These must be const — String.fromEnvironment is resolved at compile time and
returns the default if read in a non-const context.
Keep the list in a file rather than a long command line:
flutter build apk --dart-define-from-file=config/prod.json
Add those files to .gitignore if they contain anything you would not publish.
Flavours
Flavours give you separate apps — different bundle identifiers, icons, names, and backends — installable side by side.
Android, in android/app/build.gradle:
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationIdSuffix ".dev"
resValue "string", "app_name", "MyApp Dev"
}
prod {
dimension "env"
resValue "string", "app_name", "MyApp"
}
}
iOS — add matching schemes and build configurations in Xcode
(Debug-dev, Release-dev, and so on), with the bundle identifier and display
name set per configuration.
flutter run --flavor dev --dart-define-from-file=config/dev.json
flutter build appbundle --flavor prod --dart-define-from-file=config/prod.json
The flavour name must match on both platforms. See Gradle for the Android build layer.
Android signing
Generate a keystore once and keep it safe — losing it means you cannot update the app.
keytool -genkey -v -keystore upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 -alias upload
android/key.properties (never committed):
storePassword=...
keyPassword=...
keyAlias=upload
storeFile=C:/keys/upload-keystore.jks
In 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 file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
Add key.properties and *.jks to .gitignore. In CI, restore the keystore
from a base64 secret at build time and delete it afterwards.
iOS signing
Requires an Apple Developer account and a macOS machine (or a macOS CI runner).
- A distribution certificate identifies the publisher.
- A provisioning profile ties the certificate, the app identifier, and the devices together.
- Xcode's automatic signing is fine for a single developer; teams usually script
it (for example with
fastlane match) so certificates are shared reproducibly.
Set the bundle identifier, display name, deployment target, and capabilities in Xcode. Capabilities such as associated domains (for deep links, see Navigation and routing) and push notifications must match the provisioning profile or the build fails at signing.
Build artefacts
# Android
flutter build appbundle --release # .aab — required by Google Play
flutter build apk --release # single APK, for direct distribution
flutter build apk --split-per-abi # smaller per-architecture APKs
# iOS
flutter build ipa --release # produces an .ipa for upload
flutter build ios --release # build only, archive in Xcode
# Others
flutter build web --release
flutter build windows --release
Upload the .ipa with Transporter or xcrun altool; upload the .aab through
the Play Console or its publishing API.
Obfuscation and symbols
flutter build appbundle --release \
--obfuscate --split-debug-info=build/symbols/1.4.2
Obfuscation renames Dart symbols, which makes reverse engineering harder and shrinks the binary. Crash reports then arrive unreadable, so keep the symbol directory for every release you ship and upload it to your crash reporter. Archive it alongside the artefact; without it a production stack trace is useless.
Size
flutter build apk --analyze-size --target-platform android-arm64
Where the size usually goes: uncompressed images, full icon fonts, bundled media, and 3D assets — see 3D performance. Practical reductions: ship an app bundle so Play delivers per-device slices, subset icon fonts, compress images, and remove unused dependencies.
CI
A minimal pipeline that gates on quality before it builds:
name: Build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: 3.44.0
channel: stable
- run: flutter pub get
- run: flutter analyze
- run: dart format --set-exit-if-changed .
- run: flutter test --coverage
- run: flutter build appbundle --release --build-number=${{ github.run_number }}
- uses: actions/upload-artifact@v4
with:
name: appbundle
path: build/app/outputs/bundle/release/*.aab
Steps run in order and a failure aborts the job, so the build step only runs when analysis, formatting, and tests all pass. iOS builds need a macOS runner and the signing material restored from secrets.
Release checklist
- Version and build number bumped; build number higher than the last upload.
- Release notes written.
- Built in release mode, with the correct flavour and configuration.
- No debug flags, no test endpoints, no
printleft in hot paths. - Signed with the release key; symbols archived if obfuscating.
- Permissions and their usage descriptions reviewed on both platforms.
- Deep links verified on a real device, cold and warm start.
- Tested on the oldest supported OS version and on a low-end device.
- Upgrade path tested: install the previous release, create data, install over it — this is where database migrations fail. See Local storage.
- Crash reporting and analytics confirmed to be receiving events from the release build.
- Store listing, screenshots, and privacy declarations current.
Next: AI integration.