diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dc6f142..8b95081b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### 4.0.13 (20 Jul 2026) +- fix: Android hot restart now clears stale Dart callback registrations. Hot restart replaces the isolate without detaching plugins, so the registered `NativeCallable`s silently went stale - Android: the plugin now does no native work at app startup. Plugin registration and `onAttachedToEngine` are pure Java bookkeeping; the native library is loaded lazily, only when an engine-lifecycle hook actually has to call into it. Previously a static initializer pulled the whole library onto the main thread during app launch even for apps that never played a sound, and a load failure there crashed plugin registration - fix: Waveform audio sources do not match engine sample rate #501. Thanks to @Colton127 - Android now stops the audio device when idle (no active voices) like every other platform, releasing the audioserver `AudioMix` partial wakelock #250; use `setAudioDeviceIdleTimeout()` to keep it running diff --git a/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java b/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java index 3a250855..e517fdc6 100644 --- a/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java +++ b/android/src/main/java/flutter/soloud/flutter_soloud/FlutterSoloudPlugin.java @@ -1,6 +1,8 @@ package flutter.soloud.flutter_soloud; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; public final class FlutterSoloudPlugin implements FlutterPlugin { @@ -24,7 +26,9 @@ public final class FlutterSoloudPlugin implements FlutterPlugin { private static native boolean nativeClearDartCallbackRegistrationsForEngine(long engineId); - private Long engineId; + @Nullable private FlutterEngine flutterEngine; + @Nullable private Long engineId; + @Nullable private FlutterEngine.EngineLifecycleListener lifecycleListener; private static synchronized boolean ensureNativeLibraryLoaded() { if (!nativeLibraryLoadAttempted) { @@ -49,20 +53,56 @@ public void onAttachedToEngine( ) { // Deliberately does no native work. This runs during app launch for // every app that depends on the plugin, whether or not it ever uses - // SoLoud, so it must stay pure Java bookkeeping. - engineId = binding.getFlutterEngine().getEngineId(); + // SoLoud, so it must stay pure Java bookkeeping: read the engine id and + // register a listener. + final FlutterEngine engine = binding.getFlutterEngine(); + flutterEngine = engine; + engineId = engine.getEngineId(); + + lifecycleListener = new FlutterEngine.EngineLifecycleListener() { + @Override + public void onPreEngineRestart() { + // Hot restart replaces the Dart isolate but does not detach + // plugins, and the engine id is unchanged -- so without this the + // registered NativeCallables silently go stale. Only the bridges + // are cleared: the new isolate's init() finds the native engine + // still initialized and deinits it itself. + clearDartCallbackRegistrations(); + } + + @Override + public void onEngineWillDestroy() { + // Fires just before the plugin registry is destroyed, while the + // engine is still valid. + clearDartCallbackRegistrations(); + } + }; + engine.addEngineLifecycleListener(lifecycleListener); } @Override public void onDetachedFromEngine( @NonNull FlutterPluginBinding binding ) { - final Long detachedEngineId = engineId; + final FlutterEngine engine = flutterEngine; + final FlutterEngine.EngineLifecycleListener listener = lifecycleListener; + + if (engine != null && listener != null) { + engine.removeEngineLifecycleListener(listener); + } + + clearDartCallbackRegistrations(); + + flutterEngine = null; engineId = null; + lifecycleListener = null; + } - if (detachedEngineId == null || !ensureNativeLibraryLoaded()) { + private void clearDartCallbackRegistrations() { + final Long id = engineId; + if (id == null || !ensureNativeLibraryLoaded()) { return; } - nativeClearDartCallbackRegistrationsForEngine(detachedEngineId); + nativeClearDartCallbackRegistrationsForEngine(id); } }