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.groovy on Android, Swift Package Manager on iOS):

  • On Android, the host's settings.gradle includes the module and applies the module's .android/include_flutter.groovy, so :flutter becomes 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 ios in the module produces a FlutterNativeIntegration package under build/ios/SwiftPackages/; the host adds that package to its Xcode project, together with the flutter_integration.sh scheme pre-action and build phase described in Flutter's iOS project setup. Before 3.44 the only path was CocoaPods: the host's Podfile loads the module's .ios/Flutter/podhelper.rb and calls 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 ios

Two 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.swift (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 aar produces 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-framework produces 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-levelPrebuilt artifact
Flutter SDK on native CIRequiredNot needed
Dart change → visible in appImmediateBuild + publish + bump
Cross-boundary debuggingFullLimited (binaries)
Native build timeSlower (builds Dart)Faster (links binary)
Natural repo layoutMonorepo / submodulePolyrepo

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 get and 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 release host links a release Flutter 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 *.g.* files being current.

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.

On this page