Skip to main content

One post tagged with "r8-proguard"

View All Tags

Resolving MissingPluginException in Flutter Release Builds After Module Refactors

Published: · 9 min read
Sandra Rosa Antony
Software Engineer, Appxiom

When a Flutter app runs fine in debug but crashes in production with MissingPluginException, it’s usually right after a module split, add-to-app migration, or engine refactor. This post explains why flutter MissingPluginException release build errors show up only in release, what a refactor breaks, and how to make plugin registration robust across Android and iOS using the v2 embedding, R8/ProGuard keep rules, iOS linker flags, and multi-engine best practices.

Applies to:

  • Flutter 3.16+ (Dart 3.2+) up to current stable
  • Android Gradle Plugin 8.x, R8 on by default
  • iOS with CocoaPods, static frameworks

Common error signatures:

  • MissingPluginException(No implementation found for method X on channel Y)
  • generatedpluginregistrant missingpluginexception during production-only paths
  • methodchannel MissingPluginException Flutter release mode only
  • flutter release build plugin not registered after multi-module refactor

Why does MissingPluginException only happen in Flutter release builds?

Several changes make release different from debug:

  • Code shrinking/obfuscation: R8/ProGuard on Android and dead code stripping on iOS aggressively remove “unused” symbols. If your plugin registration is reflective/indirect, it can be stripped.
  • Engine lifecycle changes: After modularization, you may be manually creating engines (FlutterEngine/FlutterEngineGroup). Plugins are not auto-registered on custom engines unless you explicitly call the GeneratedPluginRegistrant.
  • Multiple engines: Each engine has its own plugin registry. Creating a second engine without re-registering plugins causes MissingPluginException.
  • Background isolates: WorkManager, audio_service, Firebase Messaging background handlers, etc., use a background Dart isolate that must register plugins separately and keep the entrypoint from being tree-shaken with @pragma('vm:entry-point').

What breaks during a module refactor?

Typical triggers:

  • Moving from a single app module to multiple Android modules or a separate Flutter module (AAR/add-to-app).
  • Switching to a cached engine or FlutterEngineGroup to boost startup.
  • Migrating legacy embedding v1 code paths.
  • Enabling R8 shrinker or tightening keep rules.
  • Changing iOS CocoaPods configuration (use_frameworks, static vs dynamic frameworks).

The effect: GeneratedPluginRegistrant isn’t run for the engine you actually use in production, or plugin symbols are removed by the linker/shrinker.

Quick checklist to fix “flutter missingpluginexception release build”

  1. Ensure v2 embedding everywhere (Android and iOS).
  2. If you create a custom FlutterEngine (or multiple engines), call GeneratedPluginRegistrant on each engine.
  3. Add R8/ProGuard keep rules for Flutter and your plugins in every host module that shrinks.
  4. On iOS, ensure -ObjC is in Other Linker Flags and plugins are correctly linked post-refactor.
  5. For background isolates, annotate entrypoints with @pragma('vm:entry-point') and follow the plugin’s background setup.
  6. Verify the release artifact actually includes GeneratedPluginRegistrant and plugin classes.
  7. Add a release-mode smoke test to CI to catch regressions.

Below are concrete, production-ready fixes.

Android: robust plugin registration in modular and multi-engine apps

1. Verify v2 embedding and call GeneratedPluginRegistrant for custom engines

If you’re not using the default FlutterActivity/FlutterFragment that creates/owns the engine, you must register plugins on each engine you create.

Kotlin (Application-level engine cache with FlutterEngineGroup):

// android/app/src/main/kotlin/com/example/app/App.kt
package com.example.app

import android.app.Application
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.FlutterEngineGroup
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugins.GeneratedPluginRegistrant

class App : Application() {
lateinit var engineGroup: FlutterEngineGroup

override fun onCreate() {
super.onCreate()

// Ensure Flutter is initialized before creating engines
val loader = FlutterInjector.instance().flutterLoader()
loader.startInitialization(this)
loader.ensureInitializationComplete(this, null)

engineGroup = FlutterEngineGroup(this)

// Create the main engine and register plugins explicitly
val mainEntrypoint = DartExecutor.DartEntrypoint(
loader.findAppBundlePath(),
"main" // or your custom entrypoint
)
val mainEngine: FlutterEngine = engineGroup.createAndRunEngine(this, mainEntrypoint)

GeneratedPluginRegistrant.registerWith(mainEngine)
FlutterEngineCache.getInstance().put("main_engine", mainEngine)
}
}

Use the cached engine from your activity/fragment:

// android/app/src/main/kotlin/com/example/app/MainActivity.kt
package com.example.app

import android.content.Context
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache

class MainActivity : FlutterActivity() {
override fun provideFlutterEngine(context: Context): FlutterEngine? {
return FlutterEngineCache.getInstance().get("main_engine")
}
}

Notes:

  • If you spin up additional engines (e.g., multiple entrypoints for features), you must call GeneratedPluginRegistrant for each engine you create.
  • If your plugin requires an Activity (e.g., Google sign-in), use FlutterFragmentActivity or ensure your plugin supports FlutterActivity.

2. R8/ProGuard keep rules to prevent stripping plugin classes

In release, R8 may remove plugin classes referenced indirectly. Keep the registrant and plugin packages.

android/app/proguard-rules.pro:

# Keep Flutter embedding and plugin registrant
-keep class io.flutter.embedding.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.plugins.GeneratedPluginRegistrant { *; }

# Keep all plugins under the io.flutter.plugins package (AAR-based)
-keep class io.flutter.plugins.** { *; }

# If your project or certain plugins live under different namespaces
# (example: federated plugins, company packages), keep them too:
-keep class com.example.plugins.** { *; }
-keep class dev.fluttercommunity.** { *; }

# Keep MethodChannel handlers that may be reflectively accessed
-keepclassmembers class ** {
@io.flutter.embedding.engine.plugins.FlutterPlugin *;
}

# Optional: if you obfuscate, keep method/channel names for easier debugging
-keepattributes *Annotation*, Signature, SourceFile, LineNumberTable

Ensure your release build uses these rules:

android/app/build.gradle:

buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}

If you’ve extracted a Flutter module into its own Android library, add equivalent keep rules to the host app’s proguard file (the host’s shrinker decides what survives).

3. Multi-module Gradle wiring after refactor

  • Make sure the host app depends on the flutter_embedding_release AAR and plugin AARs produced by the module. If you consume the Flutter module via Gradle include or mavenLocal, verify the artifacts exist for the release variant.
  • If using dynamic feature modules, host/base must see GeneratedPluginRegistrant, or you must ensure registration occurs in the module that owns the engine.

4. Background isolates (WorkManager, audio_service, Firebase)

  • Background isolates have a different engine/registry. Follow the plugin’s v2 setup and annotate your background entrypoints:
// lib/background.dart
import 'dart:ui';
import 'package:workmanager/workmanager.dart';

@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
// Use plugins here only if they support background
return Future.value(true);
});
}

Register the background dispatcher in your Android initialization per the plugin docs. Many modern plugins auto-register in the background isolate, but if you see MissingPluginException only for background tasks, registration likely didn’t happen or the entrypoint was stripped.

iOS: ensure registration survives modularization and dead code stripping

1. Register plugins on custom FlutterEngine

If you’re not using the default FlutterViewController(main) pattern, explicitly register:

Swift (AppDelegate):

import UIKit
import Flutter

@main
class AppDelegate: FlutterAppDelegate {
lazy var engine = FlutterEngine(name: "main_engine")

override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
engine.run()
GeneratedPluginRegistrant.register(with: engine)

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

Present a FlutterViewController that uses the same engine:

let vc = FlutterViewController(engine: engine, nibName: nil, bundle: nil)
window?.rootViewController = vc
window?.makeKeyAndVisible()

For multiple engines/entrypoints (e.g., feature modules), create and register each engine.

2. Linker flags: keep Objective‑C categories and Swift symbols

In Xcode, set:

  • Build Settings > Other Linker Flags: add -ObjC
  • Ensure Flutter use_frameworks! settings are compatible. Flutter pods are static by default; use_frameworks! forces dynamic frameworks and can cause duplicate or missing symbols. Prefer static unless a specific plugin requires otherwise.
  • If you vend your Flutter module as an XCFramework or integrate via add-to-app, re-run pod install after refactors and verify GeneratedPluginRegistrant.m is present in the Pods/ project under Flutter/ and references your plugins.

3. Background isolates on iOS

  • As on Android, mark background entrypoints with @pragma('vm:entry-point').
  • For plugins that support background (e.g., audio_service), follow iOS-specific setup to ensure engine initialization and registration happen before invoking plugin APIs.

Dart: common pitfalls that surface in release

  • Keep background entrypoints with @pragma('vm:entry-point') to avoid tree-shaking.
  • Don’t defer plugin initialization behind conditions that optimize away in release (e.g., assert-only paths).
  • Avoid naming mismatches between entrypoint strings used by native code and Dart top-level functions.

Example of multiple entrypoints:

// lib/main.dart
void main() => runApp(const App());

@pragma('vm:entry-point')
void featureEntrypoint() {
// Run a feature-specific app shell or background task.
}

Verify your GeneratedPluginRegistrant

  • Android: decompile the release APK/AAB with jadx and search for io.flutter.plugins.GeneratedPluginRegistrant. Confirm it lists each plugin class (e.g., io.flutter.plugins.pathprovider.PathProviderPlugin).
  • iOS: open Pods > Development Pods > Flutter > GeneratedPluginRegistrant.m and verify each plugin is registered. Re-run pod install after module moves.

If plugins are missing from the registrant, you likely have a dependency/Podspec configuration issue in the refactor.

CI: catch regressions in release mode

  • Build and run release-mode integration tests:
    • Android: ./gradlew app:assembleRelease and (optionally) run instrumentation against a release build variant.
    • iOS: xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Release build
  • Add a smoke test screen that exercises critical plugins (e.g., path_provider, shared_preferences, camera) and validates no MissingPluginException at startup.

Troubleshooting guide

  • MissingPluginException only in background tasks
    • Verify @pragma('vm:entry-point'), plugin background support, and background registration instructions.
  • Works on iOS debug, fails on iOS release
    • Check -ObjC flag and Pods integrity. Clean DerivedData and run pod repo update; pod install.
  • Works on Android debug, fails on Android release
    • Add/adjust R8 rules as above. Confirm GeneratedPluginRegistrant is present in release APK and references all plugins.
  • Using multiple Flutter engines
    • Register plugins for each engine: GeneratedPluginRegistrant.registerWith(engine).
  • Migrated from embedding v1
    • Remove old PluginRegistrantCallback patterns unless a plugin still requires it; ensure all plugins use v2 and you call GeneratedPluginRegistrant for custom engines.

Example: end-to-end setup after a multi-module refactor

  1. Android Application with engine group and registration (see above).
  2. Android R8 keep rules (see above).
  3. iOS AppDelegate with custom engine + GeneratedPluginRegistrant.
  4. Dart entrypoints annotated for any background flows.
  5. CI builds release artifacts and runs a smoke test.

With this setup, you eliminate the most common root causes of flutter proguard missingpluginexception, generatedpluginregistrant missingpluginexception, and plugin not registered scenarios after modularization.

Key takeaways

  • MissingPluginException in release usually indicates “plugin not registered on the engine you’re using” or “code stripped by shrinkers/linkers.”
  • After a module refactor or add-to-app migration, always:
    • Call GeneratedPluginRegistrant on each custom FlutterEngine (Android/iOS).
    • Add R8/ProGuard keep rules in the host module for Flutter and plugin packages.
    • Ensure -ObjC is set on iOS and Pods are consistent with your new structure.
    • Mark background entrypoints with @pragma('vm:entry-point') and follow each plugin’s background setup.
  • Add a release-mode smoke test to CI to prevent regressions.

Next steps:

  • Audit your engines and registration paths.
  • Add the keep/linker rules shown above.
  • Verify GeneratedPluginRegistrant content in your release artifacts.
  • If you’re still stuck, minimize the app to a small repro, confirm plugin registrant output, and check related GitHub issues for your specific plugins and Flutter version.

This approach will resolve the vast majority of flutter missingpluginexception release build failures, especially those introduced by multi-module and engine refactors.