How Flutter Builds Your App
Flutter compiles the same Dart code in two very different ways: one for fast iteration while you develop, and one for speed and size when you ship. Knowing which one you’re running, and what ends up in the file you upload to a store, explains a lot: why hot reload doesn’t work in a release build, why a release build won’t run on an iOS simulator, and why Shorebird can update your Dart code but not your Kotlin or Swift.
Debug, profile and release builds
Section titled “Debug, profile and release builds”Flutter has three build modes:
| Mode | How Dart runs | Use it for | Built by |
|---|---|---|---|
| Debug | JIT-compiled in the Dart VM | Development: hot reload, assertions, DevTools | flutter run |
| Profile | AOT-compiled, tracing enabled | Measuring performance on a real device | flutter run --profile |
| Release | AOT-compiled, debugging stripped | What you ship to users | flutter build <target>, flutter run --release |
In debug mode, the Dart VM compiles your code just in time (JIT) as it runs. That is what makes hot reload possible: the tool sends changed code to the running VM, which swaps it in while keeping your app’s state. It is also why debug builds are large and can feel janky. Don’t judge performance or app size from a debug build.
In profile and release mode, Flutter compiles your Dart code ahead of
time (AOT) into machine code for the device’s CPU (ARM or x64). The compiler
starts from main(), removes code nothing can reach (tree shaking), and
produces a native binary. The app starts fast and runs fast, and there is no
compiler on the device to load new code into. Hot reload only works in debug
mode, and profile and release builds don’t run on iOS simulators.
What’s inside a release build
Section titled “What’s inside a release build”A release build packages three kinds of content: your compiled Dart code, the Flutter engine with the native host app around it, and assets. Where each one lives depends on the platform.
Android
Section titled “Android”flutter build appbundle produces an Android App Bundle (.aab). Inside it:
| Path in the bundle | What it is |
|---|---|
base/lib/<abi>/libapp.so | Your Dart code, framework code and Dart packages, AOT-compiled |
base/lib/<abi>/libflutter.so | The Flutter engine (C++), including the Dart runtime |
base/dex/classes*.dex | Java and Kotlin code: your MainActivity, plugins’ Android code |
base/assets/flutter_assets/ | Assets declared in pubspec.yaml, fonts, shaders |
base/res/ | Android resources: launcher icons, strings.xml, themes |
base/lib/<abi>/ (other .so files) | Native libraries from plugins or the NDK |
<abi> is a CPU architecture such as arm64-v8a. A bundle carries one
libapp.so per architecture, and Google Play delivers each device only the one
it needs.
flutter build ipa produces an Xcode archive and an .ipa. Inside the app
bundle (Runner.app):
| Path in the app | What it is |
|---|---|
Frameworks/App.framework/App | Your Dart code, framework code and Dart packages, AOT-compiled |
Frameworks/Flutter.framework | The Flutter engine, including the Dart runtime |
Runner (the app’s executable) | Your Swift and Objective-C code, and plugins’ native code |
Frameworks/App.framework/flutter_assets/ | Assets declared in pubspec.yaml, fonts, shaders |
Assets.car, Info.plist, … | Xcode-managed resources: app icon, launch screen, settings |
Plugins can also add their own frameworks under Frameworks/.
macOS apps use the same frameworks under Contents/Frameworks/. On Linux, the
Dart code is lib/libapp.so in the build’s bundle directory, and on Windows
it is data/app.so next to the app’s executable. Both keep assets in
data/flutter_assets.
What Code Push can update, and why
Section titled “What Code Push can update, and why”Shorebird’s engine is a fork of the Flutter engine with an updater built in.
When your app starts, the engine decides which Dart code to run: the code in
libapp.so or App.framework that shipped with the release, or a patch the
updater downloaded earlier. That choice happens inside the engine, so any change
that only touches compiled Dart code can ship as a patch:
- your app’s Dart code, including widgets, state management and business logic
- Dart packages from pub.dev, as long as they don’t include native code
- values from
--dart-define, which are compiled into your Dart code
Everything else in the tables above is loaded by the operating system from the installed, store-signed app, not by the Dart VM. Shorebird doesn’t replace it, so it changes only with a new store release:
- Java, Kotlin, Swift and Objective-C code, yours and plugins’ (see Plugins and native code)
- The Flutter engine, so you can’t change your app’s Flutter version with a patch
- Assets and Android or iOS resources (see Assets in Flutter apps)
shorebird patch compares the new build with the release, warns you when native
code or assets changed, and only ships the Dart code. The
Is your change patchable? and
What types of changes can be included in a patch?
sections of the Code Push overview summarize the rules.
How a patch runs differs by platform. On Android, macOS, Windows and Linux, a
patch is a new AOT-compiled copy of your Dart code. On iOS, Apple only allows
downloaded code that is interpreted, so Shorebird runs the functions a patch
changed in an interpreter and everything else from the release’s
App.framework. System Architecture explains
the details, and Patch Performance covers when that
matters.
App size
Section titled “App size”A release build’s size is mostly the engine, your compiled Dart code, and your
assets. To see what takes the space, build with --analyze-size
(flutter build appbundle --analyze-size --target-platform android-arm64, since
size analysis takes one Android architecture, or
flutter build ios --analyze-size) and open the generated
*-code-size-analysis_*.json file in DevTools’ app size tool. Flutter’s
app size guide explains how to
estimate the download size users actually see and how to reduce it.
Shorebird’s engine adds a few megabytes to the engine library; see How big of a dependency footprint does this add? Patches are downloaded separately and stored in the app’s data directory, not added to the installed app (see What does the Shorebird updater store on disk?).
Obfuscation and debug symbols
Section titled “Obfuscation and debug symbols”--split-debug-info=<directory> moves the debug information for your Dart code
out of the binary into one symbols file per architecture (such as
app.android-arm64.symbols), which makes the app smaller. --obfuscate also
renames your classes and functions. Both are described in Flutter’s
Obfuscate Dart code guide.
Stack traces from such a build are unreadable without the symbols file from the build that produced the code. With Shorebird, that means keeping the release’s symbols and each patch’s. See Using obfuscation with Shorebird and Reading crashes from patches.