The Flutter Upgrade Passed CI. Plugins Failed on Device

The Flutter Upgrade Passed CI. Plugins Failed on Device

# flutter
The Flutter Upgrade Passed CI. Plugins Failed on DevicePaul-S

The upgrade pipeline looked perfect. flutter analyze PASS flutter test ...

The upgrade pipeline looked perfect.

flutter analyze                         PASS
flutter test                            PASS
flutter build apk --release             PASS
flutter build ios --release --no-codesign PASS
Enter fullscreen mode Exit fullscreen mode

The pull request was approved. The new build reached the testing team.

Then the device reports arrived.

Android: Push notifications never register
iOS: Sign-in returns to a blank screen
Both: Unit tests remain green
Enter fullscreen mode Exit fullscreen mode

CI had not lied. It had successfully tested what we asked it to test.

The problem was what we never asked it to test.

A Green Build Does Not Prove Plugin Behaviour

Most Flutter applications are not entirely Flutter.

A plugin usually contains a Dart-facing API and a native implementation written in Kotlin, Java, Swift, or Objective-C. The two sides communicate through platform channels or native bindings.

That creates a longer execution path:

Dart code
   ↓
Platform channel
   ↓
Plugin registration
   ↓
Android or iOS lifecycle
   ↓
Native SDK
Enter fullscreen mode Exit fullscreen mode

A Dart unit test may validate the first layer while never loading the remaining four.

Flutter’s documentation explains that plugin unit tests often replace the platform implementation with mocks. That is useful for testing application logic, but a mocked method channel cannot reveal that a native class failed to register or an operating-system callback stopped firing.

The test passes because the native failure never enters the test process.

Framework Upgrades Move Native Boundaries Too

A Flutter upgrade is rarely just a Dart SDK change.

On Android, it can affect the Android Gradle Plugin, Gradle itself, Java compatibility, Kotlin configuration, manifest handling, and plugin build scripts.

Recent Flutter migration guidance illustrates this clearly. Built-in Kotlin support changes how Android projects and plugins are configured, while add-to-app projects may require manual migration because Flutter cannot rewrite the native host application automatically.

On iOS, the move toward the UIScene lifecycle changes where application events are delivered. Plugins relying on older AppDelegate callbacks may compile correctly but fail when the application receives a URL, notification, or lifecycle event on a real device.

Compilation answers one question:

Can these files become an application?

It does not answer another:

Will every native capability behave correctly when the operating system invokes it?

Debug the Boundary, Not Just the Dart Exception

When a plugin fails after an upgrade, the Dart error is often the end of the trail rather than the beginning.

A MissingPluginException, frozen method call, or empty callback could originate from several places:

  • The plugin was not registered with the current engine.
  • The Android activity or iOS scene changed before a callback returned.
  • A required manifest entry, entitlement, or permission is missing.
  • The plugin supports debug builds but fails after release optimization.
  • Its native dependency is incompatible with the new build toolchain.
  • The plugin’s Dart package was updated without a matching native migration.

Start by reproducing the failure on one known device. Then inspect native logs through Android Studio, adb logcat, Xcode, or the macOS Console.

Follow one call across the boundary:

Did Dart send the method call?
Did the native handler receive it?
Did the operating system start the requested action?
Did the callback return?
Did Flutter receive the result?
Enter fullscreen mode Exit fullscreen mode

This is usually faster than repeatedly changing Dart code around a native failure.

Put Plugin Workflows Into the Upgrade Gate

A better upgrade pipeline includes tests that launch the application on the platforms it supports.

Flutter’s integration_test package can run tests on emulators, simulators, physical devices, and device farms. Use it for critical workflows that depend on plugins, such as:

  • Authentication redirects
  • Camera and file access
  • Push-notification registration
  • Payments and in-app purchases
  • Deep links and app links
  • Location and background services

Do not test only the happy path.

Deny a permission and request it again. Put the app in the background during a callback. Launch it from a notification. Open a deep link after the process has been terminated. Run the signed release build, not only the debug build.

The minimum upgrade evidence should look closer to this:

Static analysis                 PASS
Dart unit tests                 PASS
Android release build           PASS
iOS release build               PASS
Android plugin workflows        PASS
iOS plugin workflows            PASS
Background and cold-start tests PASS
Enter fullscreen mode Exit fullscreen mode

CI is still important. It simply needs coverage beyond compilation.

Change the Definition of “Upgrade Complete”

An upgrade is not complete when flutter build exits with code zero.

It is complete when the application’s important native paths have been exercised on Android and iOS, using the build modes and operating-system behaviours customers will actually encounter.

That may reveal that a plugin must be upgraded, patched, replaced, or temporarily pinned. It may also expose a missing native migration in the application itself.

This is the kind of evidence teams should expect from a hybrid app development company: not only a green pipeline, but a clear device matrix showing which plugin-backed workflows were tested.

The Flutter upgrade passed CI because the pipeline proved that the project could build.

The plugins failed because the application had not yet proved that it could run.

Which Flutter plugin has caused your most difficult device-only failure?