Build integration
Building from source vs consuming prebuilt artifacts (AAR / XCFramework), and what each mode means for your CI/CD.
Once you have decided where the code lives, the next question is how each native host consumes the Flutter module at build time. There are two modes - building from source, or consuming a prebuilt artifact - and the choice has direct consequences for your CI/CD, because it decides whether the native build machines need a Flutter toolchain at all.
The two modes
Source-level integration
The native build compiles the Flutter module from its Dart source as part of its own build, using the hooks the module exposes. This is what flutter create --template module sets up, and what the framework's example uses (include_flutter. on Android, Swift Package Manager on iOS):
- On Android, the host's
settings.includes the module and applies the module'sgradle ., soandroid/ include_flutter. groovy :flutterbecomes a Gradle subproject built alongside the app. - On iOS, since Flutter 3.44 the module is wired in through Swift Package Manager. Running
flutter build swift-package --platform iosin the module produces aFlutterNativeIntegrationpackage underbuild/; the host adds that package to its Xcode project, together with theios/ SwiftPackages/ flutter_integration.scheme pre-action and build phase described in Flutter's iOS project setup. Before 3.44 the only path was CocoaPods: the host'ssh Podfileloads the module's.and callsios/ Flutter/ podhelper. rb install_all_flutter_pods.
# in the Flutter module - re-run whenever the module's dependencies change;
# Dart-only changes are rebuilt by Xcode
flutter build swift-package --platform iosTwo iOS-specific things are worth knowing up front. First, CocoaPods is on its way out: Flutter keeps the CocoaPods path in maintenance mode, and the CocoaPods registry becomes read-only on 2 December 2026, so new integrations should go through SwiftPM and existing CocoaPods hosts should plan the move. Inlay itself ships both as a Swift package and as a pod. Second, as of Flutter 3.44 flutter build swift-package for a module still runs pod install internally to build plugins that have no Package. (flutter/flutter#184590), so CocoaPods has to be installed on the machine that builds the module even when the host app no longer uses it.
Until SwiftPM support landed, this was a real blocker for organizations that had already moved their native app off CocoaPods: we have had a client refuse CocoaPods outright, which left prebuilt XCFrameworks as their only option. That constraint is gone now - the same client has since moved to source integration over SwiftPM.
The practical consequence of this mode is that every machine building the native app needs the Flutter SDK installed, because compiling the app now includes compiling Dart.
What you gain is that the host always builds against real, current module source. Cross-boundary debugging works end to end, there is no publish step between changing Dart and seeing it in the app, and a checkout at a given commit is fully reproducible. For a team that owns both sides, this is the simplest and fastest development loop.
The cost is that the toolchains are now coupled. Native CI has to carry Flutter, native build times grow by the Flutter build, and native developers need a matching Flutter version on their machines. In a monorepo or submodule layout this is a natural fit; for separate native teams it is friction they did not have before.
Prebuilt-artifact integration
The Flutter module is compiled ahead of time into platform binaries, published somewhere the native build can fetch, and consumed like any other third-party dependency:
- On Android,
flutter build aarproduces an AAR plus a local Maven repository. The host adds that repo and depends on the artifact by coordinates and version. - On iOS,
flutter build ios-frameworkproduces XCFrameworks (the Flutter engine, your Dart code,FlutterPluginRegistrant, and one per plugin). The host references them via SwiftPM, CocoaPods, or directly. Flutter's docs now file this workflow under the legacy iOS project setup, so check that it still fits your Flutter version before building a pipeline on it.
The gain here is that native build machines need no Flutter toolchain - they pull a binary. The native build is faster, and the two sides are genuinely decoupled, so native teams consume Flutter through a versioned dependency, exactly like any other SDK. This is what makes a polyrepo layout practical.
The cost is a publish step between a Dart change and the app seeing it. Every change means building, versioning, and publishing artifacts, then bumping the host (see Versioning). Cross-boundary debugging is harder, because the host has binaries instead of source. And you have to build and publish every variant the host needs - a debug/profile/release set (and one per product flavor, if you have them), since a host build can only link an artifact of the matching build mode. You don't have to split by ABI, though - flutter build aar puts all Android ABIs into a single AAR per build mode.
Choosing a mode
| Source-level | Prebuilt artifact | |
|---|---|---|
| Flutter SDK on native CI | Required | Not needed |
| Dart change → visible in app | Immediate | Build + publish + bump |
| Cross-boundary debugging | Full | Limited (binaries) |
| Native build time | Slower (builds Dart) | Faster (links binary) |
| Natural repo layout | Monorepo / submodule | Polyrepo |
The choice follows repository structure closely. Source-level is the natural default in a monorepo or submodule setup - it is simpler and removes the publish round-trip. Artifact integration is what decouples a polyrepo, letting native teams stay on their own toolchain. Since the recommended destination is a monorepo, source-level integration is where most projects should end up. Reach for artifacts when organizational boundaries, build-time isolation, or a native team without Flutter on their machines make the decoupling worth the publish overhead.
CI/CD for a hybrid app
Whichever mode you pick, a hybrid app's pipeline has to account for the Flutter build somewhere. The shape differs by mode.
With source-level integration, there is one native build that happens to include Flutter:
- The Flutter SDK must be provisioned and version-pinned on the native build agents. Pin it the same way you pin Xcode and the Android SDK - a drifting Flutter version leads to flaky, hard-to-reproduce builds.
flutter pub getand code generation (inlay_gen/build_runner) have to run before the native build, so the generated boundary exists when Gradle/Xcode compile.- The Flutter build artifacts (the engine, the compiled Dart) are worth caching between runs, so the native build does not recompile Dart from scratch every time.
With artifact integration, there are effectively two pipelines:
- A module pipeline builds, versions, and publishes the AAR / XCFrameworks to an artifact registry (a Maven repo, a package registry, or a git release). This is the only place that needs Flutter.
- Each host pipeline resolves the published artifact by version and builds with no Flutter toolchain at all.
- The two are connected by versioning: publishing a new module version and bumping the hosts to it is the hand-off, and it should be automated rather than manual.
A few rules hold in both cases:
- Match build modes. A
releasehost links areleaseFlutter artifact; debug/profile/release cannot be mixed. Most artifact-integration breakages trace back to a mode mismatch. - Keep the Flutter-only checks separate and fast.
dart analyze,dart test, and code generation belong in a Flutter-side job that runs on every change to the module, independent of the slower native builds. This way Dart regressions surface in seconds rather than after a full app build. - Treat generated code as a build step. Regenerate the boundary in CI (or verify it is up to date) rather than relying on checked-in
*.files being current.g. *
The publish-and-bump hand-off that artifact integration introduces is really a versioning problem - which module version a host is pinned to, and how that moves forward. It is covered on its own in Versioning.