Android: do no native work at app startup - #21
Merged
Conversation
The plugin loaded its native library from a static initializer. GeneratedPluginRegistrant instantiates and attaches every plugin during app launch, so the whole multi-megabyte library was pulled onto the main thread before any app code referenced SoLoud -- and an UnsatisfiedLinkError there failed class initialization, crashing plugin registration outright rather than surfacing a catchable error. Plugin construction and onAttachedToEngine are now pure Java bookkeeping. The library is loaded lazily at the first point a lifecycle hook actually has to call into native code. For an app that uses SoLoud that load is a refcount bump, because Dart has already opened the same library through DynamicLibrary.open; a load failure is now swallowed rather than propagated. No Dart-side change was needed: SoLoudController is a lazy singleton and SoLoud.instance is a lazily-initialized static final, so DynamicLibrary.open does not run until app code touches SoLoud. Verified with a harness performing exactly what GeneratedPluginRegistrant does at launch -- construct, then onAttachedToEngine -- and reflecting on the private load flag afterwards: no load is attempted, in well under a millisecond of pure-Java work. The same harness confirms the detach hook still reaches native lazily and swallows a load failure instead of throwing. Compiled under -Xlint:all against stubs mirroring the real embedding API. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The plugin loaded its native library from a static initializer.
GeneratedPluginRegistrantinstantiates and attaches every plugin during app launch, so the whole multi-megabyte library was pulled onto the main thread before any app code referenced SoLoud — including for apps that never play a sound.An
UnsatisfiedLinkErrorthere also failed class initialization, crashing plugin registration outright rather than surfacing a catchable error.Approach
Plugin construction and
onAttachedToEngineare now pure Java bookkeeping. The library is loaded lazily at the first point a lifecycle hook actually has to call into native code.For an app that uses SoLoud that load is a refcount bump, because Dart has already opened the same library through
DynamicLibrary.open. A load failure is now swallowed rather than propagated — the FFI layer surfaces it from Dart where it is catchable and actionable.No Dart-side change was needed:
SoLoudControlleris a lazy singleton andSoLoud.instanceis a lazily-initializedstatic final, soDynamicLibrary.opendoes not run until app code touches SoLoud.Verification
A harness performs exactly what
GeneratedPluginRegistrantdoes at launch — construct, thenonAttachedToEngine— and reflects on the private load flag afterwards:(That 552 µs is mostly JVM class-loading in the harness itself; the actual work is a field read.)
Compiled under
-Xlint:allagainst stubs mirroring the real embedding API.flutter analyzeandflutter testunchanged.Residual
An app that depends on the plugin but never uses SoLoud will load the library once at engine destroy, to call a hook that immediately no-ops. That is a shutdown-time cost rather than a launch-time one, and does not apply to any app that actually plays audio.
Ordering
Touches only
FlutterSoloudPlugin.java. Independent of the non-Java PRs in this series, but #22 and #23 build on this file — merge this one first to avoid conflicts.Generated by Claude Code