The Dreaded MissingPluginException
Dec 27, 2020
TL:DR; ProGuard rules helped in my case. V8 removes several classes erroneously. Since V8 only does that for release builds the error doesn’t materialize on debug builds.
A month ago I mentioned a troubling carnage of a 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. Like before the release version the app 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 attributed the issue 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 (and these can imply further implication) 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 package and tooling 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 of my app and the "christmas present" was a whole series of plugin related java.
When you search for parts of this exception call stack a deep maze of MissingPluginException opens up ahead of you. You can find many issues related to this in the Flutter project’s GitHub repository. Most of them are closed and either they are not necessarily clear about the proper 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 developers and the plugin developers. An everyday developer will pick up a plugin name from the call stack and bombard the plugin developer, however 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. Verdict: we won’t be able to shoo away the problem that easily. In case of a lucky star constellation this might work but the problem will probably return in my opinion.
- Perform a flutter upgrade, clean and rebuild everything. Verdict: 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. Verdict: yet again - maybe in case of 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. Verdict: this may work as an immediate hotfix but it’s a very bad idea long term. As I mentioned: Gradle versions have consequences with respect to 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 release.
- Step back a version of plugin X. Verdict: 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. Verdict: 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:
Verdict: that’s not it again. 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:
- Some suggestions want you to add plugin specific code section like this for Firebase Messaging and shared_preferences:
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. Verdict: 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. Verdict: just reading that snippet made me predict 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 the errors away, and while doing that it also 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. Even if this worked the question would be: 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. Because last time the Android port wipe and re-scaffolding helped I kept resorting to more and more wiping. But this stopped fixing the problem. 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. Assuming that the errors messages are valid, there could be two main reasons why a class is missing from a bundle:
- 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 not even compile.
- The classes were there originally, but they got removed by something during the build process. Bingo!
There’s a build step during the Android build when R8 tries to remove any unneeded fluff from application. 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 ProGuard 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. We know that with Flutter the rendering and the meat of the application is done by Skia, which is a black box for V8. I can imagine (and this is just a suspicion) that the lack of completeness could confuse V8 and make it lean towards cutting valuable content. To be clear: lack of completeness means that the Android parts of the Flutter technology stack are mostly thin layers. That’s very different from a full blown native Android app. The Flutter ecosystem might be too new for V8.
- 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, but 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 the proguard rules fix part of my issue: the app was starting now 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 the proper track with ProGuard rules towards a solution and I gave a chance to a rule suggested at the end of that issue. Fortunately that helped.
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 one but hopefully it’ll be robust.
It’s really annoying that my MissingPluginExceptions only affect release builds, but there’s a reason for that by nature. 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 temporarily took out some mojo from my Flutter enthusiasm. I have a handful of native Android apps in the Play Store and - I might be just lucky, but - it never occured to me that only the release version would fail. I’m not sure when I will ever trust a Flutter release build in case the debug build is all good. I know that release build tests should be part of the QA process, but in case of an indie developed hobby project I like how native Android works for me.
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.