From b594cc3e1bd3985c1d0cedb011d88bbf8da3120d Mon Sep 17 00:00:00 2001 From: Stephen Saucier Date: Tue, 27 Oct 2026 14:19:35 -0400 Subject: [PATCH 1/3] fix: ignore lifecycle events after a MapView is destroyed 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/maps/android/compose/GoogleMap.kt | 2 + .../compose/MapLifecycleEventObserverTest.kt | 68 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index 552852c1..7cf8d8b6 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -489,6 +489,8 @@ internal class MapLifecycleEventObserver(private val mapView: MapView) : Lifecyc } private fun moveToLifecycleState(targetState: Lifecycle.State) { + // upFrom(DESTROYED) is null. Returning inside moveUp() would spin the loop below. + if (currentLifecycleState == Lifecycle.State.DESTROYED) return while (currentLifecycleState != targetState) { when { currentLifecycleState < targetState -> moveUp() diff --git a/maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt b/maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt new file mode 100644 index 00000000..9d71c65f --- /dev/null +++ b/maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.maps.android.compose + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.google.android.gms.maps.MapView +import io.mockk.mockk +import io.mockk.verify +import io.mockk.verifyOrder +import org.junit.Test + +public class MapLifecycleEventObserverTest { + @Test + public fun `moveToDestroyedState still walks the MapView down to onDestroy`() { + val mapView = mockk(relaxed = true) + val observer = MapLifecycleEventObserver(mapView) + setLifecycleState(observer, Lifecycle.State.RESUMED) + + observer.moveToDestroyedState() + + verifyOrder { + mapView.onPause() + mapView.onStop() + mapView.onDestroy() + } + } + + @Test + public fun `lifecycle event after DESTROYED does not throw or move the MapView`() { + val mapView = mockk(relaxed = true) + val observer = MapLifecycleEventObserver(mapView) + val owner = mockk(relaxed = true) + setLifecycleState(observer, Lifecycle.State.DESTROYED) + + observer.onStateChanged(owner, Lifecycle.Event.ON_STOP) + observer.onStateChanged(owner, Lifecycle.Event.ON_RESUME) + observer.onStateChanged(owner, Lifecycle.Event.ON_DESTROY) + observer.moveToDestroyedState() + + verify(exactly = 0) { mapView.onCreate(any()) } + verify(exactly = 0) { mapView.onStart() } + verify(exactly = 0) { mapView.onResume() } + verify(exactly = 0) { mapView.onPause() } + verify(exactly = 0) { mapView.onStop() } + verify(exactly = 0) { mapView.onDestroy() } + } + + private fun setLifecycleState(observer: MapLifecycleEventObserver, state: Lifecycle.State) { + val field = MapLifecycleEventObserver::class.java.getDeclaredField("currentLifecycleState") + field.isAccessible = true + field.set(observer, state) + } +} From c60894731ea0c5a9f134a7d31553ccddaf173df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Wed, 28 Oct 2026 12:03:50 +0100 Subject: [PATCH 2/3] chore: re-trigger CLA check From e29f470ccb5ca0437e9b3de0194c9677e98f9c22 Mon Sep 17 00:00:00 2001 From: Stephen Saucier Date: Wed, 28 Oct 2026 10:04:09 -0400 Subject: [PATCH 3/3] test: reach RESUMED and DESTROYED through the public lifecycle API Reflection on currentLifecycleState coupled the test to a private field. ON_RESUME and moveToDestroyedState land on the same states. --- .../compose/MapLifecycleEventObserverTest.kt | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt b/maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt index 9d71c65f..e10f9c1a 100644 --- a/maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt +++ b/maps-compose/src/test/java/com/google/maps/android/compose/MapLifecycleEventObserverTest.kt @@ -29,11 +29,15 @@ public class MapLifecycleEventObserverTest { public fun `moveToDestroyedState still walks the MapView down to onDestroy`() { val mapView = mockk(relaxed = true) val observer = MapLifecycleEventObserver(mapView) - setLifecycleState(observer, Lifecycle.State.RESUMED) + val owner = mockk(relaxed = true) + observer.onStateChanged(owner, Lifecycle.Event.ON_RESUME) observer.moveToDestroyedState() verifyOrder { + mapView.onCreate(any()) + mapView.onStart() + mapView.onResume() mapView.onPause() mapView.onStop() mapView.onDestroy() @@ -45,24 +49,20 @@ public class MapLifecycleEventObserverTest { val mapView = mockk(relaxed = true) val observer = MapLifecycleEventObserver(mapView) val owner = mockk(relaxed = true) - setLifecycleState(observer, Lifecycle.State.DESTROYED) + + observer.onStateChanged(owner, Lifecycle.Event.ON_RESUME) + observer.moveToDestroyedState() observer.onStateChanged(owner, Lifecycle.Event.ON_STOP) observer.onStateChanged(owner, Lifecycle.Event.ON_RESUME) observer.onStateChanged(owner, Lifecycle.Event.ON_DESTROY) observer.moveToDestroyedState() - verify(exactly = 0) { mapView.onCreate(any()) } - verify(exactly = 0) { mapView.onStart() } - verify(exactly = 0) { mapView.onResume() } - verify(exactly = 0) { mapView.onPause() } - verify(exactly = 0) { mapView.onStop() } - verify(exactly = 0) { mapView.onDestroy() } - } - - private fun setLifecycleState(observer: MapLifecycleEventObserver, state: Lifecycle.State) { - val field = MapLifecycleEventObserver::class.java.getDeclaredField("currentLifecycleState") - field.isAccessible = true - field.set(observer, state) + verify(exactly = 1) { mapView.onCreate(any()) } + verify(exactly = 1) { mapView.onStart() } + verify(exactly = 1) { mapView.onResume() } + verify(exactly = 1) { mapView.onPause() } + verify(exactly = 1) { mapView.onStop() } + verify(exactly = 1) { mapView.onDestroy() } } }