Skip to content

Release Overview and Prerequisites

Releasing a Flutter app transforms your local Dart codebase and native platform wrappers into hardened, cryptographically signed binaries. Under the hood, the Flutter build engine compiles your Dart code Ahead-of-Time (AOT), strips debug runtimes, minifies assets, and packages native platform artifacts for distribution on Google Play and the Apple App Store.

This guide explains the Flutter release lifecycle, how the build pipeline transforms your Dart and native code into store-ready artifacts, and the core architectural prerequisites for production deployments.

graph LR
    Dev["Source Code<br>(Dart + Native)"] --> Opt["Optimization<br>(AOT, Tree Shaking, Obfuscation)"]
    Opt --> Sign["Code Signing<br>(Keystore / Apple Certs)"]
    Sign --> Build["Store Artifacts<br>(AAB / IPA)"]
    Build --> Dist["Store Ingestion<br>(Google Play / App Store)"]
    Dist --> Users["Production Users"]

    style Dev fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px,color:#000
    style Opt fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000
    style Sign fill:#fff3e0,stroke:#ff9800,stroke-width:2px,color:#000
    style Build fill:#fce4ec,stroke:#e91e63,stroke-width:2px,color:#000
    style Dist fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#000
    style Users fill:#e0f2f1,stroke:#009688,stroke-width:2px,color:#000

Flutter compiles applications in three distinct build modes: Debug, Profile, and Release. Choosing the correct build mode is critical because each mode trades debugging convenience for execution performance.

FeatureDebug ModeProfile ModeRelease Mode
CompilationJust-in-Time (JIT)Ahead-of-Time (AOT)Ahead-of-Time (AOT)
Hot reload / restartSupportedDisabledDisabled
Debugging toolsFull DevTools & VM serviceDevTools tracing & profilingDisabled
Debug bannerVisibleHiddenHidden
Binary sizeLarge (includes VM & AST)Optimized for tracingFully stripped & minified
Execution speedSlower (unoptimized bytecode)Near-nativeMaximum native speed

During development, Flutter uses Dart’s JIT compiler. The Dart VM compiles code on the fly, enabling stateful hot reload and full step-debugging. However, debug binaries contain debugging symbols and the Dart VM runtime, resulting in larger file sizes and slower execution. Never ship debug builds to end users.

Profile mode compiles Dart code Ahead-of-Time to native machine code using gen_snapshot, matching the runtime performance of release mode while keeping performance tracing hooks and Flutter DevTools CPU/memory profilers active. This mode is ideal for diagnosing frame drops (jank) and memory leaks on real hardware.

Release builds apply aggressive compiler optimizations:

  • Unused code and dead branches are eliminated via tree shaking.
  • Dart code is compiled to native ARM machine code (libapp.so on Android, App.framework on iOS).
  • Debugging assertions (assert() statements), debug banners, and the VM service are completely stripped from the final binary.
  • Assets and font icons are minified.

shorebird release always builds in release mode. Without Shorebird, flutter build appbundle and flutter build ipa also build in release mode by default.

Mobile operating systems and app stores track versions through two distinct identifiers: a user-visible version string and an internal numeric build counter.

In Flutter, you define both in your pubspec.yaml file using the format version: <version-name>+<build-number>:

# pubspec.yaml
name: my_app
description: A production Flutter application.
version: 1.2.0+42

In this example:

  • 1.2.0 represents the Version Name (Semantic Versioning: MAJOR.MINOR.PATCH).
  • 42 represents the Build Number (an integer that increments with every build).

Flutter automatically injects these values into the native build configurations:

  • Android: version maps to versionName in build.gradle, and +42 maps to versionCode (see Android versioning).
  • iOS: version maps to CFBundleShortVersionString in Info.plist, and +42 maps to CFBundleVersion (see iOS versioning).

When building in automated CI/CD pipelines, you can override the values in pubspec.yaml without modifying source files by passing --build-name and --build-number:

shorebird release android --build-name=1.2.1 --build-number=43
shorebird release ios --build-name=1.2.1 --build-number=43

Shorebird records the resulting version (1.2.1+43) as the release version, and patches target it with --release-version=1.2.1+43. flutter build takes the same two flags when you build without Shorebird.

Avoid hardcoding production API keys or backend URLs in your code. Pass configuration values at compile time using --dart-define or --dart-define-from-file:

shorebird release android \
  --dart-define=API_URL=https://api.example.com \
  --dart-define=ENVIRONMENT=production

# Or load from a JSON/env configuration file:
shorebird release android \
  --dart-define-from-file=config/production.json

flutter build takes the same flags when you build without Shorebird. Pass the same values to shorebird patch for that release, so patched code reads the same configuration.

Access these variables in Dart using String.fromEnvironment():

const apiUrl = String.fromEnvironment(
  'API_URL',
  defaultValue: 'https://api.staging.example.com',
);

To protect proprietary business logic and reduce binary size, enable Dart AOT obfuscation during the release build. For further details, refer to the Flutter Obfuscation Guide.

Obfuscation replaces symbol identifiers with short, non-descriptive names and strips debug symbols. Pass the flags to shorebird release:

shorebird release android \
  --obfuscate \
  --split-debug-info=build/symbols/android

shorebird release ios \
  --obfuscate \
  --split-debug-info=build/symbols/ios
  • --obfuscate: Obfuscates Dart code names.
  • --split-debug-info=<directory>: Extracts debug symbol tables into separate symbol files rather than embedding them into the compiled binary. If you pass --obfuscate without it, Shorebird writes the files to build/shorebird/symbols.

Patches to an obfuscated release are obfuscated the same way automatically, so shorebird patch needs no --obfuscate. On iOS, the release must be built with Flutter 3.41.2 or later. See Using obfuscation with Shorebird.

Without Shorebird, flutter build appbundle and flutter build ipa take the same two flags.

When an obfuscated app crashes in production, the stack trace contains memory addresses and obscured names. Use the saved symbol file for the crashing device’s architecture to restore original file names and line numbers:

flutter symbolize \
  --input=stacktrace.txt \
  -d build/symbols/android/app.android-arm64.symbols

Once a patch is installed, its Dart frames need that patch’s symbol files: on Android every Dart frame, since a patch replaces all the compiled Dart code, and on iOS frames in code the patch changed. Pass --split-debug-info to each shorebird patch with a directory of its own. See Reading crashes from patches.

Upload these symbol files to crash reporting providers to enable automatic stack trace symbolication:

Keeping download sizes low improves conversion rates and user retention. Flutter provides built-in diagnostics to analyze binary contents (see Flutter App Size).

Generate a JSON size breakdown with a local build:

flutter build appbundle --analyze-size
flutter build ipa --analyze-size

These are diagnostic builds made with the standard Flutter. Use them to inspect size, and don’t upload them to a store: only a build from shorebird release can receive patches.

This command outputs a detailed summary of your package size and produces a size report JSON file. You can load this file into Flutter DevTools (App Size Tool) to inspect:

  • Largest compiled Dart libraries and native dependencies.
  • Asset overhead (images, audio, custom fonts).
  • Unused resource opportunities.
  1. Tree shake icons: Flutter automatically tree-shakes font icons by default in release builds (--tree-shake-icons), keeping only the glyphs actually referenced in code.
  2. Compress assets: Convert large PNG assets to WebP or vector formats (SVGs).
  3. Download large assets on demand: Fetch large models or media at runtime, or deliver them with Play Asset Delivery, instead of bundling them in the app.
  4. Android resource shrinking: Enable shrinkResources true and minifyEnabled true in build.gradle to strip unused native resources.

Manual release processes are error-prone and slow down deployment cadence. Modern teams automate compilation, code signing, and distribution using CI/CD pipelines.

Popular tools and workflows:

  • Fastlane: Open-source automation tool for generating screenshots, managing provisioning profiles (fastlane match), and deploying to stores (supply for Google Play, deliver / pilot for App Store Connect). See the Fastlane Integration Guide.
  • GitHub Actions: Cloud workflows for continuous integration and automated tag-triggered release builds. See the GitHub Actions Guide.
  • Codemagic: A CI/CD service purpose-built for Flutter that manages code signing certificates and direct store delivery. See the Codemagic Integration Guide.

Shorebird: updating production apps instantly

Section titled “Shorebird: updating production apps instantly”

A traditional app store release cycle requires compiling a new binary, submitting it to Google Play or Apple App Store, waiting for review (hours to days), and waiting for users to download the update.

graph TD
    subgraph Traditional Store Release
        A[Code Change] --> B[Full Binary Build]
        B --> C[Store Review]
        C --> D[User Downloads App Update]
    end

    subgraph Shorebird Code Push
        E[Dart Code Fix] --> F[shorebird patch]
        F --> G[Instant Cloud Delivery]
        G --> H[App Applies Patch on Next Launch]
    end

    style A fill:#ffebee,stroke:#f44336,stroke-width:2px,color:#000
    style B fill:#ffebee,stroke:#f44336,stroke-width:2px,color:#000
    style C fill:#ffebee,stroke:#f44336,stroke-width:2px,color:#000
    style D fill:#ffebee,stroke:#f44336,stroke-width:2px,color:#000

    style E fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000
    style F fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000
    style G fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000
    style H fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000

Shorebird integrates Code Push capabilities into your Flutter release strategy (see Getting Started):

  1. Create a base store release: Build your release artifact using Shorebird CLI instead of standard Flutter tooling (see Shorebird Release):
    shorebird release --platforms=android,ios
    Submit the artifact it built to Google Play and the App Store, as the Android and iOS guides describe. Don’t rebuild it with flutter build, Gradle or Xcode first.
  2. Deploy instant patches: When you need to fix a bug or push an urgent change to Dart code, deploy a patch (see Shorebird Patch):
    shorebird patch --platforms=android,ios --track=stable
    Patches go directly to users and install seamlessly in the background on their devices.

When to use a store release vs a Code Push patch

Section titled “When to use a store release vs a Code Push patch”
  • Store release required: Adding new native plugins, modifying native Android/iOS configurations (e.g., AndroidManifest.xml, Info.plist, build.gradle, Podfile), changing assets such as images and fonts, or upgrading Flutter.
  • Code Push patch supported: Modifying Dart widgets, updating business logic, adjusting styling, fixing calculation bugs, or refining state management.

Before publishing your application build to production tracks, verify every item on this checklist:

  1. Build with Shorebird

    Build every store artifact with shorebird release, and upload the artifact it built, so the release can receive patches.

  2. Increment version numbers

    Update version in pubspec.yaml with an incremented build number and proper semantic version string.

  3. Check permissions and privacy compliance

    Ensure all native permission strings in AndroidManifest.xml and Info.plist have clear, compliant descriptions. Verify that iOS Privacy Manifests are included if using required reason APIs.

  4. Verify environment and API keys

    Ensure production endpoints and keys are configured (using --dart-define or secure remote configs) instead of development credentials.

  5. Test on real devices

    Test release builds on physical iOS and Android hardware across different screen sizes and OS versions.

  6. Backup signing keys

    Store your Android release keystores, key aliases, passwords, and Apple distribution certificates in a secure credentials manager.

  7. Verify crash reporting and symbolication

    Confirm that crash reporting SDKs (Sentry, Crashlytics) receive events and that debug symbols (--split-debug-info) are uploaded and kept.

Continue to the dedicated platform guides to prepare and publish store artifacts:

Releasing for Android (Google Play)Generate an upload keystore, configure Gradle signing, build the app bundle with shorebird release, and upload it to Google Play.
Releasing for iOS (Apple App Store)Configure Xcode signing and privacy manifests, build the IPA with shorebird release, and distribute via TestFlight and App Review.
Releasing for WindowsCreate a Shorebird Windows release, then package it as an MSIX for the Microsoft Store or with an installer such as Inno Setup.