· 5 min read

Flutter Upgrades: The Native Build Work You Still Own

A Flutter upgrade is also an iOS and Android build upgrade. Here is a routine for SwiftPM, CocoaPods, AGP 9 and built-in Kotlin that keeps releases calm.

A Flutter upgrade looks like one command: flutter upgrade. In practice, it is also an upgrade of your iOS and Android builds. This is the part of the Flutter vs native trade-off that rarely makes the comparison tables. Flutter gives you one codebase for the UI and the logic, but you still own an Xcode project, a Gradle project and every native dependency your plugins pull in. This post is the routine I use so a Flutter upgrade does not turn into a broken release day.

Flutter vs native: one codebase, two build systems

A native iOS developer owns one build system. A native Android developer owns another. A Flutter developer owns both, plus the Flutter tool that sits on top of them and edits their files.

That last part matters. When you run a new Flutter version for the first time, its migrators can rewrite files in ios/ and android/. Usually that is helpful. But it means a "Dart-only" upgrade can produce a diff in Podfile, project.pbxproj, gradle.properties or build.gradle.kts, and those changes reach your release builds.

Every plugin with native code adds to this. In Mapika, ads (Unity LevelPlay) and subscriptions (Adapty) are plugins that wrap native SDKs. Each one has its own iOS and Android dependencies, and each one has to keep up with the same toolchain changes as the app.

Right now two of those changes are landing at once.

iOS: Swift Package Manager is the default now

Since Flutter 3.44, Swift Package Manager support is on by default. The first flutter run on the new version adds SwiftPM integration to the Xcode project. Plugins that support SwiftPM resolve through it. Plugins that do not yet support it still fall back to CocoaPods, so most apps keep working with both for a while.

There is a reason not to wait. The Flutter SwiftPM guide notes that the CocoaPods registry becomes read-only on December 2, 2026, and that turning SwiftPM off will not be allowed in the future. CocoaPods itself stays in maintenance mode, but new pod releases stop.

What I check on the iOS side:

If you need a way out for one release, you can turn it off per project in pubspec.yaml:

flutter:
  config:
    enable-swift-package-manager: false

Treat that as a short-term switch with a date on it, not a fix.

Android: AGP 9 and built-in Kotlin

On Android the change comes from the Android Gradle Plugin. Starting with AGP 9, you can no longer apply the Kotlin Gradle Plugin with kotlin-android. AGP has built-in Kotlin support instead.

Flutter handles this in two steps. Flutter 3.44 added AGP 9 support with built-in Kotlin turned off. Its migrator adds two lines to android/gradle.properties on the next build:

android.newDsl=false
android.builtInKotlin=false

Flutter 3.47 and later can run with built-in Kotlin turned on. To get there, the app's build.gradle.kts drops the kotlin-android plugin and moves the JVM target out of kotlinOptions:

plugins {
    id("com.android.application")
    // "kotlin-android" is gone
}

kotlin {
    compilerOptions {
        jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
    }
}

Then you set android.builtInKotlin=true and build. The built-in Kotlin migration guide has the Groovy version too.

The catch is the same as on iOS: plugins must migrate as well. If one plugin still applies the old Kotlin plugin, your app fails to build with built-in Kotlin on, even if your own code is clean. So the order is: your app first with the flag off, then check every plugin, then flip the flag.

My upgrade routine

None of this is hard on its own. It goes wrong when it happens by accident, mixed into a feature branch, a week before a release. So I keep the upgrade separate and boring.

1. Upgrade on its own branch

One branch, one change: the new Flutter version. No features, no refactors. If something breaks, there is only one suspect. I pin the version the team and CI use, so nobody upgrades by running flutter upgrade on a Monday morning.

2. Commit what the migrators changed

Run flutter pub get, then one debug build per platform. Commit the files the tool rewrote as a separate commit, with a message like "Flutter migrators: SwiftPM + gradle.properties". In review, that commit is easy to read: it contains only generated changes. Your own fixes go in the next commit.

3. Make a plugin audit list

For each plugin with native code, write down three things:

The third question is the honest one. A plugin that has not shipped in a long time will not be ready for the next toolchain change either. That is the moment to look for a replacement, or to write a thin bridge yourself, as in wrapping a native SDK with Pigeon.

4. Build release, not just debug

Debug builds hide problems. Build a release IPA and a release app bundle locally or in CI, then install them on real devices. Signing, code shrinking and dependency resolution all behave differently in release mode.

5. Ship it through the normal pipeline

Push the branch and let CI produce test builds for testers, the same way a feature would. A short Maestro smoke run on those builds catches the "app starts, then crashes on the first screen" class of problems before a store review does.

Is this a reason to go native?

No. A native app has its own upgrade days: new Xcode versions, new AGP versions, new Swift and Kotlin releases. Two native apps means two teams living through those days separately.

The difference is that Flutter makes the work less visible. The Dart side feels like the whole project, so native build work gets no time in the plan. Then it arrives anyway, at a bad moment. If you budget a small, regular slot for toolchain upgrades, Flutter stays the cheaper option for most apps. If you don't, it can feel like you have three build systems and own none of them.

Takeaways