Skip to content

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.

Flutter has three build modes:

ModeHow Dart runsUse it forBuilt by
DebugJIT-compiled in the Dart VMDevelopment: hot reload, assertions, DevToolsflutter run
ProfileAOT-compiled, tracing enabledMeasuring performance on a real deviceflutter run --profile
ReleaseAOT-compiled, debugging strippedWhat you ship to usersflutter 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.

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.

flutter build appbundle produces an Android App Bundle (.aab). Inside it:

Path in the bundleWhat it is
base/lib/<abi>/libapp.soYour Dart code, framework code and Dart packages, AOT-compiled
base/lib/<abi>/libflutter.soThe Flutter engine (C++), including the Dart runtime
base/dex/classes*.dexJava 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 appWhat it is
Frameworks/App.framework/AppYour Dart code, framework code and Dart packages, AOT-compiled
Frameworks/Flutter.frameworkThe 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.

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.

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?).

--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.