The Dreaded MissingPluginException

Dec 27, 2020

A month ago I mentioned a troubling carnage of release only malfunctions. At that time I tried a lot of various changes, a lot of them were misguided tries. It’s much harder to debug release versions and sometimes it’s like trying to feel structures in the dark. I even pinned the flutter_blue plugin version to a step back because that started to act up as well.

A few weeks later when I released the Schwinn AC Performance Plus support I ran into a release hang again. Similarly as before the release version was showing a blank screen and didn’t progress to the start screen. This rendered the app unusable and users (rightfully) punished the reputation with one star reviews in disappointment. At that time I chalked the issue up to the introduction of a new plugin for the file picker feature required by the CSV file import. However it was the same pattern: in the debug version everything was hunky-dory but the release version bonked. The Flutter technology stack has many moving parts:

  • You can be on the Flutter stable channel or the beta channel (or in extreme cases the dev channel)
  • Your Gradle Tooling version can be 3.6 (pre 4.0), 4.0, 4.1. These can have implications related to Kotlin versions or AndroidX support requirements
  • Each Flutter plugins’ android port can have AndroidX support or not
  • Each Flutter plugin has its own Gradle file with its own versions and its own Android SDK API level
  • Flutter changed the plugin API and the newest one doesn’t need any code in the MainActivity for example

I installed a release version and the christmas present was a whole series of plugin related java.lang.IllegalAccessError exceptions with a MissingPluginException bow on the top.

2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis: Rejecting re-init on previously-failed class java.lang.Class<com.mr.flutter.plugin.filepicker.FilePickerPlugin$LifeCycleObserver>: java.lang.IllegalAccessError: Interface androidx.lifecycle.b implemented by class com.mr.flutter.plugin.filepicker.FilePickerPlugin$LifeCycleObserver is inaccessible (declaration of 'com.mr.flutter.plugin.filepicker.FilePickerPlugin$LifeCycleObserver' appears in base.apk)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void io.flutter.plugins.GeneratedPluginRegistrant.registerWith(io.flutter.embedding.engine.a) (:-1)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at java.lang.Object java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) (Method.java:-2)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void io.flutter.embedding.engine.h.h.a.a(io.flutter.embedding.engine.a) (:-1)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void io.flutter.embedding.android.e.t(io.flutter.embedding.engine.a) (:-1)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void io.flutter.embedding.android.f.k(android.content.Context) (:-1)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void io.flutter.embedding.android.e.onCreate(android.os.Bundle) (:-1)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.Activity.performCreate(android.os.Bundle, android.os.PersistableBundle) (Activity.java:7148)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.Activity.performCreate(android.os.Bundle) (Activity.java:7139)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.Instrumentation.callActivityOnCreate(android.app.Activity, android.os.Bundle) (Instrumentation.java:1293)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at android.app.Activity android.app.ActivityThread.performLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.content.Intent) (ActivityThread.java:3111)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at android.app.Activity android.app.ActivityThread.handleLaunchActivity(android.app.ActivityThread$ActivityClientRecord, android.app.servertransaction.PendingTransactionActions, android.content.Intent) (ActivityThread.java:3270)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.servertransaction.LaunchActivityItem.execute(android.app.ClientTransactionHandler, android.os.IBinder, android.app.servertransaction.PendingTransactionActions) (LaunchActivityItem.java:78)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.servertransaction.TransactionExecutor.executeCallbacks(android.app.servertransaction.ClientTransaction) (TransactionExecutor.java:108)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.servertransaction.TransactionExecutor.execute(android.app.servertransaction.ClientTransaction) (TransactionExecutor.java:68)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.ActivityThread$H.handleMessage(android.os.Message) (ActivityThread.java:1986)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.os.Handler.dispatchMessage(android.os.Message) (Handler.java:106)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.os.Looper.loop() (Looper.java:215)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void android.app.ActivityThread.main(java.lang.String[]) (ActivityThread.java:6939)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at java.lang.Object java.lang.reflect.Method.invoke(java.lang.Object, java.lang.Object[]) (Method.java:-2)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run() (RuntimeInit.java:493)
2020-12-25 21:18:41.965 28894-28894/? I/indoor_exercis:     at void com.android.internal.os.ZygoteInit.main(java.lang.String[]) (ZygoteInit.java:870)
  ... 5 more
2020-12-25 21:18:41.972 28894-28894/? W/GeneratedPluginsRegister: Tried to automatically register plugins with FlutterEngine (io.flutter.embedding.engine.a@ea90390) but could not find and invoke the GeneratedPluginRegistrant.
2020-12-25 21:18:41.980 28894-28894/? D/OpenGLRenderer: Skia GL Pipeline
2020-12-25 21:18:42.066 3087-3595/? V/WindowManager: Focus changed from null to Window{e401dd4 u0 dev.csaba.track_my_indoor_exercise/dev.csaba.track_my_indoor_exercise.MainActivity}
2020-12-25 21:18:42.069 28894-28924/? I/OpenGLRenderer: Initialized EGL, version 1.4
2020-12-25 21:18:42.069 28894-28924/? D/OpenGLRenderer: Swap behavior 2
2020-12-25 21:18:42.243 3087-3338/? I/WindowManager: Window drawn AppWindowToken{eb12237 token=Token{e81a336 ActivityRecord{c759cd1 u0 dev.csaba.track_my_indoor_exercise/.MainActivity t92}}}
2020-12-25 21:18:42.245 3087-3338/? I/WindowManager:   SURFACE show Surface(name=dev.csaba.track_my_indoor_exercise/dev.csaba.track_my_indoor_exercise.MainActivity)/@0xc4eec3: dev.csaba.track_my_indoor_exercise/dev.csaba.track_my_indoor_exercise.MainActivity
2020-12-25 21:18:42.255 25083-25083/? I/GoogleInputMethodService: GoogleInputMethodService.onFinishInput():3308 
2020-12-25 21:18:42.255 25083-25083/? I/GoogleInputMethodService: GoogleInputMethodService.onStartInput():1887 
2020-12-25 21:18:42.255 3087-3146/? I/LaunchCheckinHandler: Displayed dev.csaba.track_my_indoor_exercise/.MainActivity,cp,ca,508
2020-12-25 21:18:42.255 3087-3146/? I/ActivityManager: Displayed dev.csaba.track_my_indoor_exercise/.MainActivity: +497ms
2020-12-25 21:18:42.275 26950-26950/? D/LauncherAppWidgetHost: setListenIfResumed :false, mFlags:5
2020-12-25 21:18:42.276 26950-26950/? D/LauncherAppWidgetHost: stopListening
2020-12-25 21:18:42.278 26950-26950/? W/RecentsModel: onTrimMemory level = 20
2020-12-25 21:18:42.284 6862-28204/? I/PBSessionCacheImpl: Deleted sessionId[30181003605542103] from persistence.
2020-12-25 21:18:42.294 6862-6948/? W/SearchServiceCore: Abort, client detached.
2020-12-25 21:18:42.369 28894-28912/? E/flutter: [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: MissingPluginException(No implementation found for method getAll on channel plugins.flutter.io/shared_preferences)

When you search for parts of this exception call stack a gargantuan world of MissingPluginException opens up ahead of you. You can find many issues related to this in the Flutter project’s GitHub. Most of them are closed and either it’s not necessarily clear what is the good solution or the solution marked as good doesn’t fix the problem. You can also find similar issues on any popular plugin’s Git repository as well. Some of those are closed as well and similarly to the Flutter project’s issues there’s a sea of suggestions and many of them are not applicable, clearly wrong or not working. As I mentioned before my application is not overly complicated but already has 25+ plugins. I foresee that any FLutter app developer whose app will be around for more than a year and has an Android port may come across this category of problem. And as it is right now it wears down both the app developer and the plugin developers. An average user will pick up a plugin name from the call stack and bombard the plugin developer, although the root cause of the problem is deeper.

Here are suggestions and “fixes” (intentionally in parentheses) I came across:

  • Perform a flutter clean and rebuild the project. We won’t be able to shoo away the problem that easily. In lucky star constellations this may work but the problem will return.
  • Perform a flutter upgrade, clean and rebuild everything. This may work if there were some incompatibilities related to versions but most of the people get automatic notification about newer flutter versions and since the upgrade is so easy I doubt that too many people would be too much behind anyway.
  • Perform an Invalidate Cache / Restart in Android Studio. Yet again: maybe in some rare star constellations this could help, but that’s not it.
  • Cleaning solution can go as far as: wipe the android folder and issue a flutter create in the project folder: this will scaffold the Android port from scratch. You need to save your current android folder and then merge your changes into the new scaffold. That can be done with a merge tool but it’s error prone and requires some time. Nuking the android folder helped me first for some reason but later it didn’t. And later I didn’t even bring in any new plugins.
  • Downgrade Gradle version from 4.1 to 3.6. This may work as an immediate hotfix but it’s a very bad idea long term. As I mentioned, Gradle versions have consequences of AndroidX and Kotlin version support, soon Gradle tooling version 5 will be released and you really don’t want to pin yourself to an aging version.
  • Step back a version of plugin X. This could be a short term hotfix if you can really identify a plugin which induces the error. That worked at a time when flutter_blue seemingly acted up, but I cannot pin a plugin to an old version forever. After seeing that many other plugins can cause release-only problems we’d need to find some better solution.
  • Switch from beta Flutter channel to stable channel. Could be a short-term solution again, but it won’t work unless the beta has an issue in regard.
  • Making sure calling GeneratedPluginRegistrant.registerWith(FlutterEngine(this)) in MainActivity.kt:
import android.os.Bundle
import android.os.PersistableBundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant

class MainActivity : FlutterActivity() {
  override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onCreate(savedInstanceState, persistentState)
    GeneratedPluginRegistrant.registerWith(FlutterEngine(this))
  }
}

That’s not the solution: with newer version scaffolds the MainActivity is empty, because the FlutterActivity already calls the required functions. So much so that MainAktivity.kt can actually be omitted from the source code: see 1.a of Full-Flutter app migration, I’ve made this deletion in my source code. Same goes for:

class MainActivity : FlutterActivity() {
  override fun configureFlutterEngine(@nonnull flutterEngine: FlutterEngine) {
      GeneratedPluginRegistrant.registerWith(flutterEngine)
  }
}
  • Some suggestions want you to add plugin specific code section like this for Firebase Messaging and shared_preferences:
class Application : FlutterApplication(), PluginRegistrantCallback {
  override fun onCreate() {
    super.onCreate()
    FlutterFirebaseMessagingService.setPluginRegistrant(this);
  }

  override fun registerWith(registry: PluginRegistry) {
    FirebaseCloudMessagingPluginRegistrant.registerWith(registry)
    SharedPreferencesPlugin.registerWith(registry?.registrarFor(
        "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin"));
  }
}

As I mentioned before the plugin API is changed and not the MainActivity can be removed. I don’t believe that any of these types of fiddlings would help, unless it’s a clearly identified issue of a specific plugin which is not released yet and you want to hotfix it immediately. The holy grail is not here.

  • Add <meta-data android:name=“flutterEmbedding” android:value=”2” /> to your AndroidManifest.xml. This meta tag is already part of the scaffold. I think this could be a legitimate fix if someone manually upgrades to the V2 Flutter API and forgets this tag.
  • If your problem is specifically with shared_preferences then add SharedPreferences.setMockInitialValues({}); to the beginning of your Dart main function. Just reading that snippet I wouldn’t have to try this, because I could have predicted what some users actually experienced: “actually this doesn’t work either, as the shared preference is not written anywhere, everything the user sets will be empty on the next run”. This is because the mocking shoos away errors, but doing that it swallows every call without actually doing anything. This mocking should be only used for test cases, some comments properly warn about that.
  • Some suggest increasing the minSdkVersion to 22 or higher from 19. THe question is: why would I do that if there could be a better solution which would preserve a larger set of supported devices?
  • A suggestion told to make the series of plugin initializations serial (adding await keywords) and make it robust against a single plugin failure. Some contemplated a possible race condition in this concurrent initialization area. My thought is serializing would certainly slow down start time, and if the app survives a failed plugin initialization then the crash would probably strike later when that plugin would be used.
  • Some suggested to use –no-shrink flag while building. There are two problems with that: 1. After Gradle Tooling version 5 and up android.enableR8=true will be deprecated and won’t be able to be turned off. 2. I’d like a solution which is part of the build files so it won’t matter if I perform a build from ANdroid Studio menus or the command line.
  • Some suggested to add shrinkResources false and minifyEnabled false to the build.gradle file android > buildTypes > release section. Currently the default is minifyEnabled true. There were two problems with that: 1. We would go against best practices. 2. It generated a whole bunch of errors for me at compile time, so I didn’t even get to a successful build and I just kept tumbling down the rabbit hole.

I’d like to take a little pause here because with the last two suggestions we were getting closer to the deep cause. While discovering the MissingPluginException world it was astounding to see how much everyone was desperately searching for a solution and how the suggestions were all over the place. People - including myself - spent days to navigate over false solutions and roadblocks. The issue puts so much burden on the plugin developers that there’s a ticket which inspired my blog post’s title. Since last time the android port wipe and re-scaffolding helped I kept trying to do that resorting to more and more wiping. ALong the route I tried various suggestions.

It’s always good to take a night (or a few nights) of sleep to try to clear your mind and start with a fresh head the next day. There could be two main reasons why a class is missing from a bundle:

  1. It wasn’t ever added into the bundle in the first place. For example a package which is used wasn’t included into the classpath. Since probably the source code uses the classes in question if there was a missing dependency in the build.gradle then our project would even compile.
  2. The classes were there originally, but got removed by something during the build process. Bingo!

There’s a build step where R8 tries to remove any unneeded fluff from the Android build. This has several reasons: to decrease size, to avoid duplicate classes, and many more. R8 engine is the successor of ProGuard which may sound more familiar to many. Just to see some example configuration: here is an example ProGuard config file I assembled from a Medium article. This configuration is so lengthy because sometimes R8 can be too bold and cut off too much meat from the bundle. This could be happening in our case too.

  • Certain people suggested to establish an app/proguard-rules.pro file inside the android port and add -keep class androidx.lifecycle.DefaultLifecycleObserver rule to it. First I was confused and didn’t know that R8 can be fed by ProGuard configuration rules, but ultimately it makes a lot of sense for backward compatibility. If you look back at my crash logs you can also see that it starts with a FilePickerPlugin$LifeCycleObserver IllegalAccessError. After going through so much I was skeptical, but this worked! No, not that the app was starting but I received another error which was now pointing definitely to flutter_blue plugin. The crash call stack was similar to these. I felt like I was on a good track with ProGuard rules and I gave a chance to a rule suggested at the end of that issue, and fortunately it helped.
I/flutter ( 5034): Error starting scan.
E/flutter ( 5034): [ERROR:flutter/lib/ui/ui_dart_state.cc(177)] Unhandled Exception: PlatformException(startScan, Field androidScanMode_ for b.c.a.s0 not found. Known fields are [private int b.c.a.s0.e, private b.b.a.b0$i b.c.a.s0.f, private boolean b.c.a.s0.g, private static final b.c.a.s0 b.c.a.s0.h, private static volatile b.b.a.a1 b.c.a.s0.i], java.lang.RuntimeException: Field androidScanMode_ for b.c.a.s0 not found. Known fields are [private int b.c.a.s0.e, private b.b.a.b0$i b.c.a.s0.f, private boolean b.c.a.s0.g, private static final b.c.a.s0 b.c.a.s0.h, private static volatile b.b.a.a1 b.c.a.s0.i]
E/flutter ( 5034): 	at b.b.a.v0.a(Unknown Source:72)
E/flutter ( 5034): 	at b.b.a.v0.a(Unknown Source:715)
E/flutter ( 5034): 	at b.b.a.v0.a(Unknown Source:12)
E/flutter ( 5034): 	at b.b.a.k0.a(Unknown Source:60)
E/flutter ( 5034): 	at b.b.a.k0.a(Unknown Source:49)
E/flutter ( 5034): 	at b.b.a.d1.a(Unknown Source:17)
E/flutter ( 5034): 	at b.b.a.d1.a(Unknown Source:4)
E/flutter ( 5034): 	at b.b.a.z$a.a(Unknown Source:9)
E/flutter ( 5034): 	at b.b.a.z$a.a(Unknown Source:4)
E/flutter ( 5034): 	at b.b.a.z$a.a(Unknown Source:0)
E/flutter ( 5034): 	at b.b.a.a$a.a(Unknown Source:2)
E/flutter ( 5034): 	at b.c.a.b.a(Unknown Source:10)
E/flutter ( 5034): 	at b.c.a.b.onMethodCall(Unknown Source:1414)
E/flutter ( 5034): 	at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(Unknown Source:17)
E/flutter ( 5034): 	at io.flutter.embedding.engine.dart.DartMessenger.handleMessageFromDart(Unknown Source:57)
E/flutter ( 5034): 	at io.flutter.embedding.engine.FlutterJNI.handlePlatformMessage(Unknown Source:4)
E/flutter ( 5034): 	at android.os.MessageQueue.nativePollOnce(Native Method)
E/flutter ( 5034): 	at android.os.MessageQueue.next(MessageQueue.java:336)
E/flutter ( 5034): 	at android.os.Looper.loop(Looper.java:174)
E/flutter ( 5034): 	at android.app.ActivityThread.main(ActivityThread.java:7397)
E/flutter ( 5034): 	at java.lang.reflect.Method.invoke(Native Method)
E/flutter ( 5034): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
E/flutter ( 5034): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
E/flutter ( 5034): , null)
E/flutter ( 5034): #0      FlutterBlue.scan (package:flutter_blue/src/flutter_blue.dart:124)

Looking back it completely makes sense why these issues only come in release builds: R8 only does the troubling removals in the release build pipeline. Someone might be able to disable R8 completely, but in the near future that won’t be possible. So it’s best to find a solid solution and I believe these ProGuard rules provide one. Not the prettiest solutions but hopefully it’ll be robust.

It’s really annoying that these only come by release builds, but now seeing more clearly the nature of it there’s a reason for that. I just know that it’ll cause days of carnage for other developers and even more pressure for plugin developers. This is an issue which temporariuly took out some mojo from my unicorn like Flutter enthusiasm. I have a handful of native Android apps in the Play Store and it never occured to me that only the release version would fail. I’m not sure when will be a time when I could trust a Flutter release build in case the debug build is all good. Of course with an official app release build tests are part of the QA, but in case of an indie developer hobby project I like how native Android works.

Summary: ProGuard rules helped in my case. Try to dig deep into the root cause of the problem, explore and analyze each solution, listen to your gut feelings and don’t fall into any pits.

Comments loading...