Skip to content

fix(map): eliminate cluster-renderer FATAL and harden black-map paths#5715

Merged
jamesarich merged 1 commit into
mainfrom
claude/elegant-euler-029558
Jun 2, 2026
Merged

fix(map): eliminate cluster-renderer FATAL and harden black-map paths#5715
jamesarich merged 1 commit into
mainfrom
claude/elegant-euler-029558

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

What & why

1. Cluster-renderer crash — our #1 FATAL (932 users, live on 2.7.14 (29321010))

com.google.maps.android.compose.clustering.ComposeUiClusterRenderer.renderViewToBitmapDescriptorIllegalStateException: Composed into the View which doesn't propagate ViewTreeLifecycleOwner!

Passing clusterItemContent makes maps-compose rasterize the node chip through an off-screen ComposeView that has no reachable ViewTreeLifecycleOwner/SavedStateRegistryOwner, fired from the cluster renderer's async Handler — a race the owner-propagation band-aids (#5704/#5708) could never win.

Fix: replace the Composable cluster path with a custom DefaultClusterRenderer<NodeClusterItem> that paints chips as Canvas BitmapDescriptors (the same approach #5702 used for plain markers). No ComposeView is ever created, so the crash class is eliminated, not raced. Precision circles are preserved by exposing the renderer's unclustered-item set (the library's clusterItemDecoration hook is internal-gated and won't fire for a non-library renderer, so we draw the Circles directly).

2. "All black Google Maps" — app-side fallback

MapView set MapType.NONE whenever a custom tile provider URL was selected. If getTileProvider() returned null (bad {x}/{y}/{z} template, missing local MBTiles), the TileOverlay was skipped and the base map stayed NONEsolid black, no recourse.

Fix: resolve the provider once (hoisted/remembered) and use MapType.NONE only when a working provider exists; otherwise fall back to the user's selected base map, with a Logger.w so a broken source is diagnosable.

3. Centralized Maps init + renderer logging (diagnostics)

New google-flavor MapsSdkInitializer: keeps the synchronous single-arg MapsInitializer.initialize (preserves #5709's BitmapDescriptorFactory readiness guarantee) and registers the renderer callback once to log which renderer (LATEST/LEGACY) actually loaded, via Kermit (→ Crashlytics/Datadog through the existing sanctioned writers). The LEGACY renderer was decommissioned in March 2025 — we can only observe the renderer, not force it — so this turns the documented "Latest-renderer goes black for some users" reports into something correlatable in field telemetry.

Reviewer notes

  • google-flavor only — fdroid uses OSMDroid and never references Maps; both :androidApp:compileGoogleDebugKotlin and :androidApp:compileFdroidDebugKotlin pass, as do spotlessCheck and detekt.
  • Behavioral trade-off: cluster chips lose the pulse-on-recent-heard animation (now static Canvas chips, matching the existing NodeTrack/Traceroute markers). Worth it to kill a 932-user FATAL.
  • Known limitation (out of scope): a custom provider that builds but whose tiles 404 at runtime still shows NONE underneath — needs server-side tile validation, a separate effort.
  • Not yet done: full assembleDebug/test/allTests baseline and on-device verification (cluster markers render + no crash; broken custom tile → base map; logcat renderer/fallback lines). CI will run the full suite.

🤖 Generated with Claude Code

The maps-compose cluster renderer was our #1 FATAL (932 users on the live
2.7.14/29321010 build): passing `clusterItemContent` makes the library
rasterize the chip through an off-screen ComposeView with no reachable
ViewTreeLifecycleOwner, fired from an async Handler that the #5704/#5708
owner-propagation band-aids could not win against.

Replace the Composable cluster path with a custom DefaultClusterRenderer
that paints node chips as Canvas BitmapDescriptors (mirroring the #5702
marker migration), so no ComposeView is ever created and the crash class is
eliminated rather than raced. Precision circles are preserved by exposing the
renderer's unclustered-item set (the library's clusterItemDecoration hook is
internal-gated and won't fire for a custom renderer). Trade-off: cluster chips
are now static, matching the existing track/traceroute chips.

Also address the "all black Google Maps" reports:
- MapType.NONE fallback: only blank the Google base map when a custom tile
  provider actually builds; if it fails (bad {x}/{y}/{z} template, missing
  MBTiles), fall back to the selected base map instead of rendering NONE
  (solid black with no recourse). Log a warning on fallback.
- Centralize Maps SDK init in a google-flavor MapsSdkInitializer: keep the
  synchronous single-arg initialize (preserves #5709's BitmapDescriptorFactory
  readiness) and register the renderer callback once to log which renderer
  (LATEST/LEGACY) actually loaded via Kermit. LEGACY is decommissioned, so we
  can only observe, not force — this turns "black for some users" reports into
  something correlatable in Crashlytics/Datadog.

All changes are google-flavor only (fdroid uses OSMDroid). Both flavors compile;
spotlessCheck and detekt pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the bugfix PR tag label Jun 2, 2026
@jamesarich jamesarich enabled auto-merge June 2, 2026 17:03
@jamesarich jamesarich added this pull request to the merge queue Jun 2, 2026
Merged via the queue into main with commit 0f123ad Jun 2, 2026
18 checks passed
@jamesarich jamesarich deleted the claude/elegant-euler-029558 branch June 2, 2026 17:24
jamesarich added a commit that referenced this pull request Jun 3, 2026
…ine-map crash structurally

The late-May/early-June crash-fix flurry introduced app-side MapsInitializer
calls from Compose to work around a "BitmapDescriptorFactory is not initialized"
crash on the node-detail inline map (#5709), then extended it to the main map
(#5715). maps-compose initializes the SDK itself when a GoogleMap creates its
MapView, and the idiom is to build BitmapDescriptors inside a live map.
Initializing from Compose (ahead of / racing the MapView's own init) correlates
with the LATEST-renderer vector base map rendering black on affected devices
("Normal" black while satellite/terrain/hybrid render fine). Open-tag bisection
of a field report (fine at open.16/17, black at open.20) places the inline-map
regression at open.19 and the main-map regression at open.20.

Revert to the pre-flurry, library-idiomatic initialization:
- delete MapsSdkInitializer; remove all app-side MapsInitializer calls (the
  MapView init LaunchedEffect and MarkerBitmapRenderer's ensureMapsInitialized;
  buildNodeChipDescriptor drops its now-unused Context param)
- fix the inline-map crash structurally: build the marker descriptor inside
  InlineMap's GoogleMap content, where the SDK is already initialized, exactly
  like the main map (which never crashed)
- drop the redundant play-services-maps version pin; maps-compose pulls
  20.0.0 transitively via maps-ktx

Every descriptor build site now sits inside a live map, so no app-side init is
needed and the crash cannot recur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant