· 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:
- Which plugins still need CocoaPods. After the upgrade, open
ios/Podfile.lock. Anything still listed there is a plugin without SwiftPM support. That list is your risk list. - The minimum deployment target. If you raise it, regenerate the config with
flutter build ios --config-onlyso the Xcode project and the packages agree. - CI. If your iOS pipeline runs
pod installin a script, it now runs next to SwiftPM resolution, not instead of it. On Xcode Cloud that script lives inci_post_clone.sh, which I covered in Xcode Cloud for Flutter.
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:
- Does it support SwiftPM, or is it still in
Podfile.lock? - Does it build with
android.builtInKotlin=true? - When was its last release?
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
- A Flutter upgrade is also an Xcode and Gradle upgrade. Plan it as one.
- Since Flutter 3.44, SwiftPM is the default on iOS. Check
Podfile.lockto see which plugins still depend on CocoaPods. - AGP 9 removes the old way of applying Kotlin. Migrate your app with the flag off, check plugins, then turn on built-in Kotlin.
- Keep upgrades on their own branch, with the migrator changes in a separate commit.
- Audit native plugins by their last release date. An abandoned plugin is a future broken build.