From 4b44c2cc835f5c586efe4a355515baa9b26d1c60 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 13:16:50 -0800 Subject: [PATCH 1/3] Stop and destroy the MapView when the map leaves the composition Only the Activity's real ON_DESTROY destroyed the MapView, so a provider switch left it alive with the controller it reaches. It is now stopped and destroyed through MapViewBase's onDestroyMapView, once. --- .../com/mapconductor/mapbox/MapboxMapView.kt | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index f2a0f8d..bf8b3cb 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -58,7 +58,6 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.ContextWrapper -import android.view.ViewGroup @SuppressLint("Lifecycle") @Composable @@ -221,20 +220,6 @@ fun MapboxMapView( // Do not call here to keep the MapView instance // holderRef.value?.mapView?.onPause() } - - @SuppressLint("Lifecycle") - override fun onDestroy(owner: LifecycleOwner) { - val currentHolder = holderRef.value - if (currentHolder != null) { - val activity = context.findActivity() - if (activity?.isChangingConfigurations == true) { - (currentHolder.mapView.parent as? ViewGroup)?.removeView(currentHolder.mapView) - } else { - // Ensure these calls are safe if mapView might be null or already destroyed - currentHolder.mapView.onDestroy() - } - } - } } lifecycle.addObserver(observer) onDispose { @@ -242,6 +227,13 @@ fun MapboxMapView( } } }, + // Started in viewProvider, so stopped before it is destroyed. A MapView + // left alive keeps its renderer and, through its listeners, the + // controller with every overlay. + onDestroyMapView = { mapView -> + mapView.onStop() + mapView.onDestroy() + }, ) } From 41c525962ac2a368a470130ea9099e8276c4a517 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 15:19:03 -0800 Subject: [PATCH 2/3] Give the MapView its own lifecycle and end it when the map leaves Mapbox's lifecycle plugin registers component callbacks on the Activity and observes the view tree's lifecycle, and lets go only when that lifecycle is destroyed; MapView.onDestroy() does not undo it. Under the Activity's lifecycle every MapView the user switched away from (and through its click listeners the controller) stayed until the Activity went. The MapView now gets a MapViewLifecycleOwner, ended in onDestroyMapView: the plugin then stops and destroys it and unregisters what it registered. --- .../com/mapconductor/mapbox/MapboxMapView.kt | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt index bf8b3cb..847e722 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxMapView.kt @@ -11,9 +11,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeLifecycleOwner import com.mapbox.maps.MapInitOptions import com.mapbox.maps.MapView import com.mapconductor.compose.map.MapViewBase +import com.mapconductor.compose.map.MapViewLifecycleOwner import com.mapconductor.core.OnCameraMoveHandler import com.mapconductor.core.OnMapEventHandler import com.mapconductor.core.OnMapLoadedHandler @@ -116,6 +119,12 @@ fun MapboxMapView( ) MapView(context, mapOptions).also { + // Mapbox's lifecycle plugin follows the view tree's lifecycle and + // only lets go of the MapView (component callbacks, observers) + // when that lifecycle is destroyed. With the Activity's, every + // MapView the user switched away from would stay until the + // Activity goes, so it gets one of its own; see onDestroyMapView. + it.setViewTreeLifecycleOwner(MapViewLifecycleOwner(lifecycle)) it.onStart() } }, @@ -227,12 +236,18 @@ fun MapboxMapView( } } }, - // Started in viewProvider, so stopped before it is destroyed. A MapView - // left alive keeps its renderer and, through its listeners, the - // controller with every overlay. + // Ending the MapView's own lifecycle has the lifecycle plugin stop and + // destroy it and unregister what it registered. A MapView left alive + // keeps its renderer and, through its listeners, the controller with + // every overlay. onDestroyMapView = { mapView -> - mapView.onStop() - mapView.onDestroy() + val owner = mapView.findViewTreeLifecycleOwner() as? MapViewLifecycleOwner + if (owner != null) { + owner.destroy() + } else { + mapView.onStop() + mapView.onDestroy() + } }, ) } From ce827fa51ad421c9379524184675cf2b3c88e3c9 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 16:20:26 -0800 Subject: [PATCH 3/3] Read the controller through the state base, which MapViewBase clears The state kept its own controller field, set on attach and never cleared, so a state the app keeps held the destroyed controller and every overlay of the old map. It now reads the base's attached controller, which MapViewBase detaches when the map is torn down. The polygon conductor's destroy() clears its polygon and outline managers, as core's PolygonController now does. --- .../java/com/mapconductor/mapbox/MapboxViewStateImpl.kt | 6 ++++-- .../mapconductor/mapbox/polygon/MapboxPolygonConductor.kt | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt b/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt index 1e0adea..57fa0f0 100644 --- a/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt +++ b/src/main/java/com/mapconductor/mapbox/MapboxViewStateImpl.kt @@ -23,7 +23,10 @@ class MapboxViewState( cameraPosition: MapCameraPosition = MapCameraPosition.Default, ) : MapViewState(cameraPosition), MapboxViewStateInterface { - private var controller: MapboxMapViewControllerInterface? = null + // Read through the base so that letting go of the controller there (MapViewBase + // does it when the map is torn down) leaves nothing behind here. + private val controller: MapboxMapViewControllerInterface? + get() = attachedMapController as? MapboxMapViewControllerInterface private var _mapDesignType: MapboxDesignType = mapDesignType @@ -35,7 +38,6 @@ class MapboxViewState( get() = _mapDesignType internal fun setController(controller: MapboxMapViewControllerInterface) { - this.controller = controller attachController(controller) } diff --git a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt index d797101..9599813 100644 --- a/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt +++ b/src/main/java/com/mapconductor/mapbox/polygon/MapboxPolygonConductor.kt @@ -129,7 +129,10 @@ class MapboxPolygonConductor( } override fun destroy() { - // No native resources to clean up for polygons + // A destroyed conductor can still be referenced (by its map controller, + // or a state that has not let go yet); without this it keeps every entity. + polygonOverlay.polygonManager.clear() + polylineOverlay.polylineManager.clear() } }