Skip to content

fix: release GoogleMap listeners and dispose subcomposition when showing/hiding map - #1039

Open
dkhawk wants to merge 2 commits into
mainfrom
fix/581-oom-showing-hiding-map
Open

dkhawk wants to merge 2 commits into
mainfrom
fix/581-oom-showing-hiding-map

Conversation

@dkhawk

@dkhawk dkhawk commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes memory retention and teardown ordering issues when GoogleMap is repeatedly shown/hidden (if (showMap) { GoogleMap(...) }) or deactivated/reused via ReusableContentHost / LazyColumn:

  1. Detach GoogleMap Listeners and InfoWindowAdapter on Clear:
    • MapApplier.attachClickListeners() registers 9 click/drag listeners and ComposeInfoWindowAdapter(mapView, ...) on GoogleMap (wrapped in Binder stubs that hold strong references to MapApplier and MapView), but MapApplier.onClear() never detached them. Added MapApplier.detachClickListeners() and invoked it from onClear().
    • MapPropertiesNode.onRemoved() and onCleared() now clear the 4 camera listeners (setOnCameraIdleListener(null), setOnCameraMoveCanceledListener(null), setOnCameraMoveStartedListener(null), setOnCameraMoveListener(null)) and setLocationSource(null) in addition to cameraPositionState.setMap(null).
    • TileOverlayNode.onRemoved() and onCleared() now clear tileOverlayState.tileOverlay = null.
  2. Synchronous Subcomposition Disposal & MapView Teardown:
    • Scoped subcompositionJob and subcomposition (Composition) to MapTagData on the MapView instance so onReset and onRelease dispose the map subcomposition synchronously before lifecycleObserver.moveToDestroyedState() (mapView.onDestroy()) is invoked, and so update cleanly re-launches the subcomposition when a deactivated MapView is reused.
    • Switched launchSubcomposition to Dispatchers.Main.immediate and guarded after mapView.awaitMap() (if (!isActive || mapView.tagDataOrNull !== tagData) return@launch) so rapid show/hide cycles where onRelease runs before getMapAsync completes do not throw a NullPointerException on mapView.tag or create an orphaned Composition.
    • Cleaned up removeOnAttachStateChangeListener, lifecycle observer registration, setViewTreeLifecycleOwner(null), setViewTreeSavedStateRegistryOwner(null), and removeAllViews() in onRelease.

Testing

  • Added unit tests in MapApplierCleanupTest verifying that MapApplier.onClear(), MapPropertiesNode, TileOverlayNode, and MarkerNode detach all listeners, InfoWindowAdapter, LocationSource, and state references.
  • Added instrumentation tests testShowingHidingMapRepeatedlyReleasesMapViews and testReusableContentHostResetAndReuse in GoogleMapViewTests.
  • Verified with heap dump (hprof) analysis across 30 show/hide cycles that 0 GoogleMap / MapApplier / ComposeInfoWindowAdapter instances remain in the heap.

Fixes #581

map.setOnGroundOverlayClickListener(null)
map.setOnPolygonClickListener(null)
map.setOnPolylineClickListener(null)
map.setOnMarkerClickListener(null)
map.setOnPolygonClickListener(null)
map.setOnPolylineClickListener(null)
map.setOnMarkerClickListener(null)
map.setOnInfoWindowClickListener(null)
map.setOnMarkerClickListener(null)
map.setOnInfoWindowClickListener(null)
map.setOnInfoWindowCloseListener(null)
map.setOnInfoWindowLongClickListener(null)
map.setOnInfoWindowClickListener(null)
map.setOnInfoWindowCloseListener(null)
map.setOnInfoWindowLongClickListener(null)
map.setOnMarkerDragListener(null)
map.setOnInfoWindowCloseListener(null)
map.setOnInfoWindowLongClickListener(null)
map.setOnMarkerDragListener(null)
map.setInfoWindowAdapter(null)
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Coverage (unit tests)

Compared against 0bc6ecd on main (#1040).

Module Line % Change Branch % Change
maps-compose 12.09% 🔺 +12.09 2.19% 🔺 +2.19
maps-compose-utils 2.06% no change 0.49% no change
maps-compose-widgets 0.00% no change 0.00% no change
TOTAL 9.44% 🔺 +9.02 1.82% 🔺 +1.73

Line and branch coverage from unit test reports. History is recorded on the coverage-history branch after each merge to main.

@dkhawk
dkhawk marked this pull request as ready for review October 13, 2026 23:58
@dkhawk
dkhawk requested a review from kikoso October 13, 2026 23:58
@googlemaps-bot

googlemaps-bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Coverage (instrumentation tests)

Overall Project 44.77% -0.06% 🍏
Files changed 91.54% 🍏

Module Coverage
maps-app 63.91% -0.16% 🍏
Files
Module File Coverage
maps-app MapUpdater.kt 81.62% -0.44% 🍏
GoogleMap.kt 80.47% -0.55% 🍏
MapApplier.kt 41.21% 🍏
TileOverlay.kt 0% -2.52% ❌

…ing/hiding map

Ensure MapApplier, MapPropertiesNode, and TileOverlayNode detach all GoogleMap listeners, InfoWindowAdapter, LocationSource, and state references when cleared or removed. Dispose the map subcomposition synchronously in AndroidView's onReset and onRelease callbacks before destroying the MapView, clean up view tree owners and attach-state listeners, and guard against rapid show/hide cycles where MapView is released before awaitMap() completes.

Fixes #581
@dkhawk
dkhawk force-pushed the fix/581-oom-showing-hiding-map branch from e1c2594 to 3b093fd Compare October 14, 2026 17:25
@dkhawk
dkhawk requested a review from LoyalAbbas October 14, 2026 19:50

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OutOfMemory exception when showing/hiding GoogleMaps composable

3 participants