Skip to content

fix: ignore lifecycle events after a MapView is destroyed - #1055

Open
spsaucier wants to merge 5 commits into
googlemaps:mainfrom
spsaucier:fix/no-event-up-from-destroyed
Open

spsaucier wants to merge 5 commits into
googlemaps:mainfrom
spsaucier:fix/no-event-up-from-destroyed

Conversation

@spsaucier

@spsaucier spsaucier commented Oct 27, 2026 •

Copy link
Copy Markdown
  • Make sure to open a GitHub issue as a bug/feature request before writing your code! That way we can discuss the change, evaluate designs, and agree on the general idea
  • Ensure the tests and linter pass
  • Code coverage does not decrease (if any source code was changed)
  • Appropriate docs were updated (if necessary)

Fixes #1054

MapLifecycleEventObserver.moveToLifecycleState walks a MapView with Lifecycle.Event.upFrom. DESTROYED is the minimum state, so upFrom(DESTROYED) is null and moveUp() throws IllegalStateException: no event up from DESTROYED.

MapView.onRelease() on main already detaches the observer before moveToDestroyedState(), which covers Compose disposal. The throw remains when the observer is still registered and a lifecycle event arrives after destroy. Released tags through 9.0.1 only remove the observer from onViewDetachedFromWindow. Returning inside moveUp() would not fix this: while (currentLifecycleState != targetState) would never finish. The guard returns at the start of moveToLifecycleState when the state is already DESTROYED. moveToDestroyedState only enters that function while the state is above INITIALIZED, so the path down to onDestroy is unchanged.

This matches a production crash on maps-compose 6.10.0, whose moveUp is the same method as current main (symbolicated to the error(...) line). 29 Android crashes in 30 days on React Native 0.85.3; 22 of them were immediately after the app was backgrounded. Median time since the last interaction was about 3.9s. Sample device: Samsung SM-A166U, Android 16.

./gradlew spotlessKotlinCheck passed. :maps-compose:testDebugUnitTest --tests com.google.maps.android.compose.MapLifecycleEventObserverTest passed (2 tests): a lifecycle event after DESTROYED does not throw or call MapView, and moveToDestroyedState from RESUMED still calls onPause, onStop, and onDestroy.

upFrom(DESTROYED) is null, so a lifecycle event after destroy throws
"no event up from DESTROYED". Returning inside moveUp() would spin the
state loop, so moveToLifecycleState returns before that loop.
@google-cla

google-cla Bot commented Oct 27, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@spsaucier

Copy link
Copy Markdown
Author

I've signed the CLA now.

@kikoso kikoso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I've signed the CLA now.

Thanks, @spsaucier ! The CLA check is still failing, but it is not about your signature: the fix commit (b594cc3e) is authored as @saucier-consulting with a different email than the one on your CLA, and the bot checks every commit author. Could you either amend it with the email covered by your CLA (git commit --amend --reset-author, then force-push, which also drops my empty commit), or add that email and account to your CLA and use the rescan link in the check?

The fix itself looks good to me, so LGTM once the CLA is green. Thanks for such a detailed issue, it made this very easy to review!

verify(exactly = 0) { mapView.onDestroy() }
}

private fun setLifecycleState(observer: MapLifecycleEventObserver, state: Lifecycle.State) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: setting currentLifecycleState through reflection couples the test to the private field name. We could reach the same states through the public API instead, e.g. onStateChanged(owner, Lifecycle.Event.ON_RESUME) to get to RESUMED and moveToDestroyedState() to get to DESTROYED. Not a blocker though.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Dropped the reflection in e29f470. Both tests now reach RESUMED with onStateChanged(owner, ON_RESUME) and DESTROYED with moveToDestroyedState(). Events after that still do not add MapView calls.

saucier-consulting and others added 3 commits October 28, 2026 10:04
Reflection on currentLifecycleState coupled the test to a private field.
ON_RESUME and moveToDestroyedState land on the same states.

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.

MapLifecycleEventObserver.moveUp throws "no event up from DESTROYED" after AndroidView.onRelease

4 participants