Skip to content

refactor(map): adopt maps-compose 8.4.0 stock clustering, drop custom renderer workaround - #6301

Merged
jamesarich merged 2 commits into
mainfrom
claude/maps-compose-840-cluster-cleanup
Jul 16, 2026
Merged

refactor(map): adopt maps-compose 8.4.0 stock clustering, drop custom renderer workaround#6301
jamesarich merged 2 commits into
mainfrom
claude/maps-compose-840-cluster-cleanup

Conversation

@jamesarich

@jamesarich jamesarich commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Bumps maps-compose 8.3.1 → 8.4.0 and retires the custom cluster-renderer workaround that upstream has now fixed. Supersedes #6298, which cannot land on its own: 8.4.0 transitively bumps android-maps-utils to 5.0.0, a source-breaking Kotlin-conversion major, so the version bump must ship together with the call-site adaptations in this PR.

Why now: 8.4.0 carries two fixes we want on the google flavor:

  • #931: custom Compose info windows render empty on compose-ui 1.10+ (we ship 1.11.4 and use MarkerInfoWindowComposable for track points), because compose-ui 1.10 added an isShown guard that makes detached views draw nothing.
  • #930: ComposeUiClusterRenderer now sets ViewTreeSavedStateRegistryOwner/ViewTreeLifecycleOwner on its off-screen render views and guards render callbacks with scope.isActive — the root cause of our former top Crashlytics FATAL (Composed into the View which doesn't propagate ViewTreeLifecycleOwner!), which NodeClusterMarkers worked around with a custom renderer and pre-baked bitmaps.

Changes

🛠️ Refactoring & Architecture

  • NodeClusterMarkers.kt rewritten to the stock Clustering(...) API (167 → 88 lines): the custom DefaultClusterRenderer subclass, per-node BitmapDescriptor baking, and the MapEffect/SideEffect/re-cluster wiring are all deleted. Node chips compose via clusterItemContent (safe as of upstream [Bug] Trying to change fixed position coords fails #930), per-item z-order flows through the new ClusteringMarkerProperties helper, precision circles draw via the new clusterItemDecoration slot, and minClusterSize = 10 is applied through the new onClusterManager hook. Native info windows (title/snippet) and click navigation are unchanged. Side benefit: the 1-second "just heard" pulse on node chips now actually animates — the bake path froze it at frame 0.

🧹 Chores (android-maps-utils 5.0.0 source-compat)

  • gradle/libs.versions.toml: maps-compose 8.3.1 → 8.4.0 (pulls maps-ktx 6.2.0 / android-maps-utils 5.0.0).
  • NodeClusterItem: ClusterItem is a Kotlin interface with properties in utils 5.0.0 — getter overrides converted to override val position/title/snippet/zIndex.
  • MapView.kt: isLayerOnMap moved off the Layer base class (now dispatched per concrete GeoJsonLayer/KmlLayer); Feature.geometry became private (getGeometry()/getGeometryType()); GeoJson style setters became explicit functions (setStrokeColor/setStrokeWidth/setWidth).
  • Note: utils 5.0.0 deprecates the whole com.google.maps.android.data KML/GeoJSON layer stack in favor of a new platform-agnostic data layer, and the deprecated bridge classes have known rendering quirks in 5.0.0 (e.g. invisible point markers, MultiPolygon style drops). This PR keeps the minimal source-compat fixes so the bump can land; a full migration of the custom map-layer overlay onto the new data-layer pipeline is already in progress on a separate branch and will supersede the safeAddLayerToMap/applySimpleStyleSpec code touched here.

Testing Performed

  • spotlessCheck, detekt, assembleDebug, test, allTests all green locally.
  • Emulator E2E (google debug, 200-node sanitized Burning Man capture replayed over TCP): node chips render with correct colors/emoji, a "10+" cluster bubble forms (minClusterSize honored), native info windows show title/snippet, info-window tap navigates to node details, and repeated fast back-navigation during active clustering produced 0 crashes (the exact scenario of the old FATAL).
  • Precision-circle rendering could not be exercised (replay positions carry no precision_bits); the gating logic is condition-identical to the previous implementation.

Summary by CodeRabbit

  • New Features
    • Improved node cluster marker rendering by leveraging the built-in clustering content/decoration flow.
    • Precision circles now render reliably for unclustered nodes when enabled.
    • Marker priority (user node and favorites) is preserved via z-index handling.
  • Bug Fixes
    • Prevented duplicate map layer additions for GeoJSON/KML layers.
    • Improved GeoJSON styling by improving geometry type detection and applying stroke/width updates more consistently.
  • Chores
    • Updated maps-compose to 8.4.0 for improved compatibility.

… renderer workaround

maps-compose 8.4.0 fixes the two issues that forced NodeClusterMarkers onto a
custom DefaultClusterRenderer with pre-baked bitmaps: ComposeUiClusterRenderer
now sets ViewTreeLifecycleOwner/ViewTreeSavedStateRegistryOwner on its
off-screen render views and guards callbacks with scope.isActive (upstream
googlemaps/android-maps-compose#930 — the root cause of our former top
Crashlytics FATAL), and custom Compose info windows no longer render blank on
compose-ui 1.10+ (#931).

- NodeClusterMarkers: rewritten to the stock Clustering() API (167 -> 88
  lines). Chips compose via clusterItemContent, per-item z-order via
  ClusteringMarkerProperties, precision circles via clusterItemDecoration,
  minClusterSize via the onClusterManager hook. Native info windows and
  click navigation unchanged.
- The bump pulls android-maps-utils 5.0.0 (source-breaking Kotlin
  conversion): NodeClusterItem moves to property overrides; MapView adapts
  isLayerOnMap dispatch, Feature.getGeometry()/getGeometryType(), and
  GeoJson style setter functions.

Verified on emulator (google debug, 200-node replay): chips, 10+ cluster
bubble, info windows, info-window navigation, and 0 crashes under repeated
fast back-navigation during clustering.

Supersedes #6298.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f6085f0e-239e-4523-9a16-8f2242a3edf9

📥 Commits

Reviewing files that changed from the base of the PR and between f57ac47 and 779805b.

📒 Files selected for processing (1)
  • androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt

📝 Walkthrough

Walkthrough

Map layer registration and GeoJSON styling now use explicit layer checks and setter APIs. Cluster markers use composable content and decorations, with cluster sizing configured through the cluster manager. The maps-compose version was upgraded to 8.4.0.

Changes

Map rendering

Layer / File(s) Summary
Clustered marker composition
gradle/libs.versions.toml, androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt, androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
The maps-compose version was upgraded, cluster item methods were converted to properties, and node markers now use composable content, precision-circle decorations, and cluster-manager minimum-size configuration.
Map layer and GeoJSON styling
androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
Layer registration now checks supported layer types explicitly, while GeoJSON styling uses geometry accessors and setter APIs for polygon and line-string styling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Clustering
  participant NodeClusterItem
  participant PulsingNodeChip
  participant DefaultClusterRenderer
  participant Circle
  Clustering->>NodeClusterItem: Read position and zIndex properties
  Clustering->>PulsingNodeChip: Render unclustered node marker
  Clustering->>Circle: Draw precision circle when enabled
  Clustering->>DefaultClusterRenderer: Apply minimum cluster size
  DefaultClusterRenderer->>DefaultClusterRenderer: Re-cluster items
Loading

Suggested labels: chore

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Accurately summarizes the main change: upgrading maps-compose and replacing the custom cluster renderer with stock clustering.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt (1)

51-66: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Extract precisionMap to avoid repeated allocations.

getPrecisionMeters() is called for every node during map clustering, which happens frequently on the UI thread. Re-instantiating a map of 10 items on every invocation causes unnecessary memory allocation and garbage collection pressure. Move precisionMap to a companion object so it is initialized only once.

⚡ Proposed fix
-    fun getPrecisionMeters(): Double? {
-        val precisionMap =
-            mapOf(
-                10 to 23345.484932,
-                11 to 11672.7369,
-                12 to 5836.36288,
-                13 to 2918.175876,
-                14 to 1459.0823719999053,
-                15 to 729.53562,
-                16 to 364.7622,
-                17 to 182.375556,
-                18 to 91.182212,
-                19 to 45.58554,
-            )
-        return precisionMap[this.node.position.precision_bits]
-    }
+    companion object {
+        private val PRECISION_MAP = mapOf(
+            10 to 23345.484932,
+            11 to 11672.7369,
+            12 to 5836.36288,
+            13 to 2918.175876,
+            14 to 1459.0823719999053,
+            15 to 729.53562,
+            16 to 364.7622,
+            17 to 182.375556,
+            18 to 91.182212,
+            19 to 45.58554,
+        )
+    }
+
+    fun getPrecisionMeters(): Double? = PRECISION_MAP[this.node.position.precision_bits]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt`
around lines 51 - 66, Move the precisionMap definition out of
getPrecisionMeters() and into the NodeClusterItem companion object so the map is
initialized once and reused across calls. Update getPrecisionMeters() to look up
node.position.precision_bits from that shared map, preserving the existing
nullable result and precision values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt`:
- Around line 1378-1386: Update MapView.kt sites 1351-1358, 1378-1386, and 1395
to use property access for the maps-utils APIs: replace isLayerOnMap(),
feature.getGeometry()?.getGeometryType(), setStrokeColor(...),
setStrokeWidth(...), and setWidth(...) with their corresponding properties,
preserving the existing values and behavior.

In `@gradle/libs.versions.toml`:
- Line 65: Update the maps-compose version declaration to a published,
resolvable release, using 8.3.0 or another confirmed published version instead
of 8.4.0.

---

Outside diff comments:
In
`@androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt`:
- Around line 51-66: Move the precisionMap definition out of
getPrecisionMeters() and into the NodeClusterItem companion object so the map is
initialized once and reused across calls. Update getPrecisionMeters() to look up
node.position.precision_bits from that shared map, preserving the existing
nullable result and precision values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 72033d8f-c3b4-4271-bf79-13b147cb4302

📥 Commits

Reviewing files that changed from the base of the PR and between 8403fc5 and f57ac47.

📒 Files selected for processing (4)
  • androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/component/NodeClusterMarkers.kt
  • androidApp/src/google/kotlin/org/meshtastic/app/map/model/NodeClusterItem.kt
  • gradle/libs.versions.toml

Comment thread androidApp/src/google/kotlin/org/meshtastic/app/map/MapView.kt
Comment thread gradle/libs.versions.toml
Review follow-up: getPrecisionMeters() runs per unclustered item on every
cluster pass; allocate the bits->meters map once instead of per call.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesarich
jamesarich merged commit a0484e7 into main Jul 16, 2026
3 checks passed
@jamesarich
jamesarich deleted the claude/maps-compose-840-cluster-cleanup branch July 16, 2026 18:06
@github-actions github-actions Bot added the refactor no functional changes label Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor no functional changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant