Fix NPE crash on the V2X map when messages arrive during teardown

osmdroid's MapView.onDetach() permanently tears the view down: afterwards its
MapViewRepository holds a null MapView, so constructing a Marker against it
throws NullPointerException from inside InfoWindow's constructor.

It was being called from a DisposableEffect keyed on the lifecycle owner, which
disposes independently of the AndroidView that owns the map. The update block
could therefore still run against an already-detached MapView and rebuild its
markers:

  java.lang.NullPointerException: Attempt to invoke virtual method
    'MapViewRepository MapView.getRepository()' on a null object reference
      at org.osmdroid.views.overlay.Marker.<init>(Marker.java:116)
      at V2xLiveMapViewKt...(V2xLiveMapView.kt:119)

The crash is dated 2026-08-17 18:19, ten minutes after DENM reception went live
on the device. The defect was always there, but every incoming message
recomposes this view, so going from occasional updates to one per second made
the window easy to land in - and SPATEM at ~2 Hz makes it easier still.

Moves the teardown to AndroidView's onRelease, which is the callback that means
"this View is gone" and after which Compose guarantees no further update.
This commit is contained in:
Ashin Walpola
2026-08-20 16:31:41 +02:00
parent a5ad3dcc5d
commit eb6150260b
@@ -85,7 +85,7 @@ fun V2xLiveMapView(
lifecycleOwner.lifecycle.addObserver(observer) lifecycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose {
lifecycleOwner.lifecycle.removeObserver(observer) lifecycleOwner.lifecycle.removeObserver(observer)
mapViewRef.value?.onDetach() // onDetach() deliberately NOT called here - see AndroidView's onRelease below.
} }
} }
@@ -173,6 +173,20 @@ fun V2xLiveMapView(
mv.controller.animateTo(ownGeoPoint) mv.controller.animateTo(ownGeoPoint)
mv.invalidate() mv.invalidate()
}, },
// osmdroid's onDetach() permanently tears the MapView down: afterwards its
// MapViewRepository holds a null MapView, so constructing a Marker against it throws
// NullPointerException from deep inside InfoWindow's constructor.
//
// This used to run in the DisposableEffect's onDispose, which is NOT safe: that effect
// is keyed on the lifecycle owner and disposes independently of this AndroidView, so
// the update block above could still run against an already-detached MapView and
// rebuild its markers. It crashed the app on 2026-08-17 once DENMs started arriving,
// because every incoming message recomposes this view and there are far more updates
// to land in that window than there used to be.
//
// onRelease is the callback that actually means "this View is gone": Compose
// guarantees no further update after it.
onRelease = { it.onDetach() },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }