From 06d69924d0dad5b853e92446ee862626c56134b9 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 13:16:37 -0800 Subject: [PATCH 1/5] Release the map view when MapViewBase leaves the composition MapViewBase destroyed the controller on dispose but nobody destroyed the SDK's map view. Its render thread kept the view, and through the camera listeners the old controller with every overlay, so each provider switch left the previous map in memory until the app ran out of it. MapViewBase now takes onDestroyMapView. On dispose it destroys the controller, detaches the view and hands it to the provider once. This also runs for a configuration change: the old Activity's view is never shown again, and what survives a rotation is the saved state. A provider that does not pass it gets a warning in the log instead of a silent leak. The previous signature stays as a hidden overload so code compiled against it still links (same JVM descriptor; the .api line differs only in the erased generic signature). --- api/android-sdk-compose.api | 3 +- .../mapconductor/compose/map/MapViewBase.kt | 95 ++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/api/android-sdk-compose.api b/api/android-sdk-compose.api index d5e97f2..e811940 100644 --- a/api/android-sdk-compose.api +++ b/api/android-sdk-compose.api @@ -61,7 +61,8 @@ public final class com.mapconductor.compose.map.MapAttributionOverlayKt { public static final void MapAttributionOverlay(java.util.List, androidx.compose.runtime.Composer, int); } public final class com.mapconductor.compose.map.MapViewBaseKt { - public static final , SpecificController extends com.mapconductor.core.controller.BaseMapViewController, ActualMapView extends android.view.View, ActualMap, SpecificScope extends com.mapconductor.compose.MapViewScope, SpecificHolder extends com.mapconductor.core.map.MapViewHolderInterface> void MapViewBase(SpecificState, androidx.compose.runtime.MutableState, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, SpecificScope, com.mapconductor.core.map.MapOverlayRegistry, kotlin.jvm.functions.Function1, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function4, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); + public static final , SpecificController extends com.mapconductor.core.controller.BaseMapViewController, ActualMapView extends android.view.View, ActualMap, SpecificScope extends com.mapconductor.compose.MapViewScope, SpecificHolder extends com.mapconductor.core.map.MapViewHolderInterface> void MapViewBase(SpecificState, androidx.compose.runtime.MutableState, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, SpecificScope, com.mapconductor.core.map.MapOverlayRegistry, kotlin.jvm.functions.Function1, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function4, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); + public static final void MapViewBase(com.mapconductor.core.map.MapViewStateInterface, androidx.compose.runtime.MutableState, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, com.mapconductor.compose.MapViewScope, com.mapconductor.core.map.MapOverlayRegistry, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2, kotlin.jvm.functions.Function2, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function4, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); } public final class com.mapconductor.compose.marker.LocalMarkerCollectorKt { public static final androidx.compose.runtime.ProvidableCompositionLocal> getLocalMarkerCollector(); diff --git a/src/main/java/com/mapconductor/compose/map/MapViewBase.kt b/src/main/java/com/mapconductor/compose/map/MapViewBase.kt index 8d762d8..46f3cb3 100644 --- a/src/main/java/com/mapconductor/compose/map/MapViewBase.kt +++ b/src/main/java/com/mapconductor/compose/map/MapViewBase.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -109,6 +110,17 @@ fun < customDisposableEffect: (@Composable (InitState, Ref) -> Unit)? = null, style: MapViewStyle? = null, onStyleDiagnostics: ((List) -> Unit)? = null, + /** + * Releases the SDK's map view after the map has left the composition. + * + * Called once, on the main thread, after the controller has been destroyed + * and the view has been taken out of its parent. Every provider has to pass + * this: a map view nobody destroys keeps its render thread (and through its + * listeners the controller with every overlay) alive until the process runs + * out of memory. It runs for a configuration change too -- the old Activity's + * view is never shown again; what survives a rotation is the state. + */ + onDestroyMapView: ((ActualMapView) -> Unit)? = null, content: (@Composable SpecificScope.() -> Unit)? = null, ) { ResourceProvider.init(LocalContext.current) @@ -450,16 +462,95 @@ fun < LaunchedEffect(state.uiSettings, initState) { controllerRef.value?.applyUISettings(state.uiSettings) } + val currentOnDestroyMapView by rememberUpdatedState(onDestroyMapView) DisposableEffect(Unit) { - // Setup logic when the composable enters the screen - onDispose { + // Overlays first, while the map they live on is still alive. controllerRef.value?.destroy() + controllerRef.value = null + holderRef.value = null + val mapView = mapViewRef.value ?: return@onDispose + mapViewRef.value = null + destroyMapView( + mapView = mapView, + provider = state::class.java.simpleName, + onDestroyMapView = currentOnDestroyMapView, + ) } } customDisposableEffect?.invoke(initState, holderRef) } +/** + * The signature before `onDestroyMapView`, kept so code compiled against it + * still links. It cannot release the map view; see the overload above. + */ +@Deprecated("Pass onDestroyMapView so the map view is released", level = DeprecationLevel.HIDDEN) +@Composable +fun < + SpecificState : MapViewStateInterface<*>, + SpecificController : BaseMapViewController, + ActualMapView : View, + ActualMap : Any, + SpecificScope : MapViewScope, + SpecificHolder : MapViewHolderInterface, +> MapViewBase( + state: SpecificState, + cameraState: MutableState, + modifier: Modifier = Modifier, + viewProvider: @Composable () -> ActualMapView, + scope: SpecificScope, + registry: MapOverlayRegistry, + sdkInitialize: suspend () -> Boolean = { true }, + holderProvider: suspend (mapView: ActualMapView) -> SpecificHolder, + controllerProvider: suspend (holder: SpecificHolder) -> SpecificController, + onMapLoaded: OnMapLoadedHandler? = null, + customDisposableEffect: (@Composable (InitState, Ref) -> Unit)? = null, + style: MapViewStyle? = null, + onStyleDiagnostics: ((List) -> Unit)? = null, + content: (@Composable SpecificScope.() -> Unit)? = null, +) = MapViewBase( + state = state, + cameraState = cameraState, + modifier = modifier, + viewProvider = viewProvider, + scope = scope, + registry = registry, + sdkInitialize = sdkInitialize, + holderProvider = holderProvider, + controllerProvider = controllerProvider, + onMapLoaded = onMapLoaded, + customDisposableEffect = customDisposableEffect, + style = style, + onStyleDiagnostics = onStyleDiagnostics, + onDestroyMapView = null, + content = content, +) + +private fun destroyMapView( + mapView: ActualMapView, + provider: String, + onDestroyMapView: ((ActualMapView) -> Unit)?, +) { + if (onDestroyMapView == null) { + // Not passing it compiles and the map still draws; only the memory goes. + Log.w( + "MapConductor", + "$provider does not release its map view (MapViewBase onDestroyMapView is null); " + + "the old map view and its controller stay in memory", + ) + return + } + // Detach before destroying, so the view's surface goes away while the SDK + // renderer is still there to handle it. + (mapView.parent as? ViewGroup)?.removeView(mapView) + try { + onDestroyMapView(mapView) + } catch (e: Exception) { + Log.w("MapConductor", "$provider: releasing the map view failed", e) + } +} + @Composable private fun BasicMessage(text: String) { Box( From cb498c11c03c15dcad0cdbf19c65f71c94fa94db Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 15:19:03 -0800 Subject: [PATCH 2/5] Add MapViewLifecycleOwner, a map view's own lifecycle Some SDKs keep what a map view holds -- lifecycleScope coroutines, observers, component callbacks -- until the lifecycle they were given (or found in the view tree) is destroyed. With the Activity's, a map the user switched away from stays until the Activity goes. MapViewLifecycleOwner follows the host lifecycle and can be ended on its own from onDestroyMapView. --- api/android-sdk-compose.api | 5 ++ .../compose/map/MapViewLifecycleOwner.kt | 47 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/main/java/com/mapconductor/compose/map/MapViewLifecycleOwner.kt diff --git a/api/android-sdk-compose.api b/api/android-sdk-compose.api index e811940..55ce351 100644 --- a/api/android-sdk-compose.api +++ b/api/android-sdk-compose.api @@ -64,6 +64,11 @@ public final class com.mapconductor.compose.map.MapViewBaseKt { public static final , SpecificController extends com.mapconductor.core.controller.BaseMapViewController, ActualMapView extends android.view.View, ActualMap, SpecificScope extends com.mapconductor.compose.MapViewScope, SpecificHolder extends com.mapconductor.core.map.MapViewHolderInterface> void MapViewBase(SpecificState, androidx.compose.runtime.MutableState, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, SpecificScope, com.mapconductor.core.map.MapOverlayRegistry, kotlin.jvm.functions.Function1, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function4, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); public static final void MapViewBase(com.mapconductor.core.map.MapViewStateInterface, androidx.compose.runtime.MutableState, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, com.mapconductor.compose.MapViewScope, com.mapconductor.core.map.MapOverlayRegistry, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2, kotlin.jvm.functions.Function2, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function4, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); } +public final class com.mapconductor.compose.map.MapViewLifecycleOwner implements androidx.lifecycle.LifecycleOwner { + public com.mapconductor.compose.map.MapViewLifecycleOwner(androidx.lifecycle.Lifecycle); + public androidx.lifecycle.Lifecycle getLifecycle(); + public final void destroy(); +} public final class com.mapconductor.compose.marker.LocalMarkerCollectorKt { public static final androidx.compose.runtime.ProvidableCompositionLocal> getLocalMarkerCollector(); } diff --git a/src/main/java/com/mapconductor/compose/map/MapViewLifecycleOwner.kt b/src/main/java/com/mapconductor/compose/map/MapViewLifecycleOwner.kt new file mode 100644 index 0000000..99285ee --- /dev/null +++ b/src/main/java/com/mapconductor/compose/map/MapViewLifecycleOwner.kt @@ -0,0 +1,47 @@ +package com.mapconductor.compose.map + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry + +/** + * A lifecycle for one map view that follows the host's but can end on its own. + * + * Some SDKs tie what a map view holds to the [LifecycleOwner] it is given or + * finds in the view tree -- coroutines in its `lifecycleScope`, observers, + * component callbacks -- and let go of it only when that lifecycle is + * destroyed. Given the Activity's, a map the user switched away from stays + * until the Activity goes. Give the view this instead and call [destroy] from + * `onDestroyMapView`. + * + * Until [destroy] it moves with [host] (it catches up on creation). [destroy] + * stops following and moves it down to DESTROYED, which the SDK sees as + * ON_PAUSE / ON_STOP / ON_DESTROY as far as it had got. Calling it again, or + * after the host was destroyed, does nothing. + */ +class MapViewLifecycleOwner( + private val host: Lifecycle, +) : LifecycleOwner { + private val registry = LifecycleRegistry(this) + + private val follow = + LifecycleEventObserver { _, event -> + if (registry.currentState != Lifecycle.State.DESTROYED) registry.handleLifecycleEvent(event) + } + + init { + host.addObserver(follow) + } + + override val lifecycle: Lifecycle + get() = registry + + fun destroy() { + host.removeObserver(follow) + // INITIALIZED cannot go straight to DESTROYED; nothing was started then. + if (registry.currentState.isAtLeast(Lifecycle.State.CREATED)) { + registry.currentState = Lifecycle.State.DESTROYED + } + } +} From 34b14ddb5d646829f644eee2121f193d593d1ac6 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 16:20:26 -0800 Subject: [PATCH 3/5] Detach the destroyed controller from the state; drop the old signature MapViewBase now hands the controller it destroyed back to the state (MapViewState.detachController), so a state the app keeps no longer holds the old map's controller and overlays. Done here for every provider; ArcGIS did it on its own before. The hidden overload without onDestroyMapView is removed: code compiled against it has to be rebuilt (binary break, accepted). --- api/android-sdk-compose.api | 1 - .../mapconductor/compose/map/MapViewBase.kt | 55 +++---------------- 2 files changed, 8 insertions(+), 48 deletions(-) diff --git a/api/android-sdk-compose.api b/api/android-sdk-compose.api index 55ce351..ac630bf 100644 --- a/api/android-sdk-compose.api +++ b/api/android-sdk-compose.api @@ -62,7 +62,6 @@ public final class com.mapconductor.compose.map.MapAttributionOverlayKt { } public final class com.mapconductor.compose.map.MapViewBaseKt { public static final , SpecificController extends com.mapconductor.core.controller.BaseMapViewController, ActualMapView extends android.view.View, ActualMap, SpecificScope extends com.mapconductor.compose.MapViewScope, SpecificHolder extends com.mapconductor.core.map.MapViewHolderInterface> void MapViewBase(SpecificState, androidx.compose.runtime.MutableState, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, SpecificScope, com.mapconductor.core.map.MapOverlayRegistry, kotlin.jvm.functions.Function1, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function2, ? extends java.lang.Object>, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function4, ? super androidx.compose.runtime.Composer, ? super java.lang.Integer, kotlin.Unit>, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.Unit>, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); - public static final void MapViewBase(com.mapconductor.core.map.MapViewStateInterface, androidx.compose.runtime.MutableState, androidx.compose.ui.Modifier, kotlin.jvm.functions.Function2, com.mapconductor.compose.MapViewScope, com.mapconductor.core.map.MapOverlayRegistry, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function2, kotlin.jvm.functions.Function2, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function4, com.mapconductor.core.map.MapViewStyle, kotlin.jvm.functions.Function1, kotlin.jvm.functions.Function3, androidx.compose.runtime.Composer, int, int, int); } public final class com.mapconductor.compose.map.MapViewLifecycleOwner implements androidx.lifecycle.LifecycleOwner { public com.mapconductor.compose.map.MapViewLifecycleOwner(androidx.lifecycle.Lifecycle); diff --git a/src/main/java/com/mapconductor/compose/map/MapViewBase.kt b/src/main/java/com/mapconductor/compose/map/MapViewBase.kt index 46f3cb3..ed268b0 100644 --- a/src/main/java/com/mapconductor/compose/map/MapViewBase.kt +++ b/src/main/java/com/mapconductor/compose/map/MapViewBase.kt @@ -61,6 +61,7 @@ import com.mapconductor.core.map.MapDesignTypeInterface import com.mapconductor.core.map.MapOverlayRegistry import com.mapconductor.core.map.MapPaddings import com.mapconductor.core.map.MapViewHolderInterface +import com.mapconductor.core.map.MapViewState import com.mapconductor.core.map.MapViewStateInterface import com.mapconductor.core.map.MapViewStyle import com.mapconductor.core.map.MapViewStyleEffect @@ -466,7 +467,13 @@ fun < DisposableEffect(Unit) { onDispose { // Overlays first, while the map they live on is still alive. - controllerRef.value?.destroy() + controllerRef.value?.let { controller -> + controller.destroy() + // The state outlives the map (apps keep it, e.g. one per provider + // while switching); without this it holds the destroyed controller + // and with it every overlay of the old map. + (state as? MapViewState<*>)?.detachController(controller) + } controllerRef.value = null holderRef.value = null val mapView = mapViewRef.value ?: return@onDispose @@ -481,52 +488,6 @@ fun < customDisposableEffect?.invoke(initState, holderRef) } -/** - * The signature before `onDestroyMapView`, kept so code compiled against it - * still links. It cannot release the map view; see the overload above. - */ -@Deprecated("Pass onDestroyMapView so the map view is released", level = DeprecationLevel.HIDDEN) -@Composable -fun < - SpecificState : MapViewStateInterface<*>, - SpecificController : BaseMapViewController, - ActualMapView : View, - ActualMap : Any, - SpecificScope : MapViewScope, - SpecificHolder : MapViewHolderInterface, -> MapViewBase( - state: SpecificState, - cameraState: MutableState, - modifier: Modifier = Modifier, - viewProvider: @Composable () -> ActualMapView, - scope: SpecificScope, - registry: MapOverlayRegistry, - sdkInitialize: suspend () -> Boolean = { true }, - holderProvider: suspend (mapView: ActualMapView) -> SpecificHolder, - controllerProvider: suspend (holder: SpecificHolder) -> SpecificController, - onMapLoaded: OnMapLoadedHandler? = null, - customDisposableEffect: (@Composable (InitState, Ref) -> Unit)? = null, - style: MapViewStyle? = null, - onStyleDiagnostics: ((List) -> Unit)? = null, - content: (@Composable SpecificScope.() -> Unit)? = null, -) = MapViewBase( - state = state, - cameraState = cameraState, - modifier = modifier, - viewProvider = viewProvider, - scope = scope, - registry = registry, - sdkInitialize = sdkInitialize, - holderProvider = holderProvider, - controllerProvider = controllerProvider, - onMapLoaded = onMapLoaded, - customDisposableEffect = customDisposableEffect, - style = style, - onStyleDiagnostics = onStyleDiagnostics, - onDestroyMapView = null, - content = content, -) - private fun destroyMapView( mapView: ActualMapView, provider: String, From 39a52ded9e349e59441c8c087f764399212a3f79 Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 17:21:31 -0800 Subject: [PATCH 4/5] Detach from the state the controller was attached to; drop style mutations A cycle through every provider (Boston parcels) still kept two old maps: - ArcGIS 2D: the example hands the same MapViewBase a second state for a frame (ArcGIS 3D and 2D share a state type), the provider attached the controller to that one, and dispose detached it from the first. MapViewBase now remembers the state current when the controller was made and asks it, the latest and the first to let go (each only if it is still attached). - Mapbox: the VectorStyleMutationSupport store left in the state's registry holds the map's holder and controller. MapViewBase removes it with the other two capabilities it already took down. --- .../mapconductor/compose/map/MapViewBase.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mapconductor/compose/map/MapViewBase.kt b/src/main/java/com/mapconductor/compose/map/MapViewBase.kt index ed268b0..d049d37 100644 --- a/src/main/java/com/mapconductor/compose/map/MapViewBase.kt +++ b/src/main/java/com/mapconductor/compose/map/MapViewBase.kt @@ -66,6 +66,7 @@ import com.mapconductor.core.map.MapViewStateInterface import com.mapconductor.core.map.MapViewStyle import com.mapconductor.core.map.MapViewStyleEffect import com.mapconductor.core.map.ScreenProjectionRequirement +import com.mapconductor.core.map.VectorStyleMutationSupportKey import com.mapconductor.core.map.VectorStyleSupportKey import com.mapconductor.core.map.resolveMapAttributions import com.mapconductor.core.marker.MarkerCapableInterface @@ -127,6 +128,7 @@ fun < ResourceProvider.init(LocalContext.current) val mapViewRef = remember { Ref() } val controllerRef = remember { Ref() } + val controllerStateRef = remember { Ref() } val holderRef = remember { Ref() } var initState by remember { mutableStateOf(InitState.NotStarted) } val bubbles by scope.bubbleFlow.collectAsState() @@ -145,6 +147,9 @@ fun < onDispose { state.serviceRegistry.remove(MarkerRenderingSupportKey) state.serviceRegistry.remove(VectorStyleSupportKey) + // Mapbox / MapLibre register a store whose callback holds the map's + // holder and controller; left here it keeps the whole old map. + state.serviceRegistry.remove(VectorStyleMutationSupportKey) } } @@ -454,6 +459,11 @@ fun < mapViewRef.value?.let { mapView -> val holder = holderProvider(mapView) holderRef.value = holder + // The provider attaches the controller to the state it was composed + // with now, which need not be the one this effect first saw: an app + // can hand the same MapViewBase another state (the example does, for + // a frame, between ArcGIS 3D and 2D). + controllerStateRef.value = state controllerRef.value = controllerProvider(holder) initState = InitState.MapCreating } @@ -464,6 +474,7 @@ fun < controllerRef.value?.applyUISettings(state.uiSettings) } val currentOnDestroyMapView by rememberUpdatedState(onDestroyMapView) + val currentState by rememberUpdatedState(state) DisposableEffect(Unit) { onDispose { // Overlays first, while the map they live on is still alive. @@ -471,9 +482,13 @@ fun < controller.destroy() // The state outlives the map (apps keep it, e.g. one per provider // while switching); without this it holds the destroyed controller - // and with it every overlay of the old map. - (state as? MapViewState<*>)?.detachController(controller) + // and with it every overlay of the old map. Each state lets go only + // if this controller is the one attached to it. + setOf(controllerStateRef.value, currentState, state).forEach { + (it as? MapViewState<*>)?.detachController(controller) + } } + controllerStateRef.value = null controllerRef.value = null holderRef.value = null val mapView = mapViewRef.value ?: return@onDispose From 365f7538acd4e8111c75a6859672b1c85a02103e Mon Sep 17 00:00:00 2001 From: Masashi Katsumata Date: Sat, 7 Nov 2026 19:19:16 -0800 Subject: [PATCH 5/5] CI: build against core's branch of the same name when there is one A change that adds API to core and uses it here is opened as a pair of PRs on branches of the same name. CI always built core's default branch, so the compose PR failed until core was merged (fix/mapview-teardown: detachController(controller) not found). Use core's same-named branch when it exists; otherwise the default branch, as before. --- .github/workflows/build.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bfa6b3..54cf398 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,9 +37,21 @@ jobs: java-version: '17' cache: 'gradle' + # core と同時に出した変更(同じ名前のブランチ)は、その core と組む。 + # 既定ブランチの core では、まだ入っていない API を使う PR が必ず落ちる。 + # core に同じ名前のブランチが無ければ、これまでどおり既定ブランチを使う。 - name: Build core locally + env: + HEAD_REF: ${{ github.head_ref }} run: | - git clone --depth 1 https://github.com/MapConductor/android-sdk-core.git _core + CORE=https://github.com/MapConductor/android-sdk-core.git + if [ -n "$HEAD_REF" ] && git ls-remote --exit-code --heads "$CORE" "$HEAD_REF" > /dev/null; then + echo "core: $HEAD_REF" + git clone --depth 1 --branch "$HEAD_REF" "$CORE" _core + else + echo "core: default branch" + git clone --depth 1 "$CORE" _core + fi cd _core ./gradlew --no-daemon publishToMavenLocal cd ..