From 191807e8eaa063476dc391f62c2bdc4adcc17637 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Fri, 22 Aug 2025 15:32:56 +0300 Subject: [PATCH 1/2] fix: view dispose crash on android if view is on background --- .../maps/flutter/navigation/GoogleMapView.kt | 49 +++++++---- .../navigation/GoogleMapsAutoMapView.kt | 19 +++-- .../navigation/GoogleMapsBaseMapView.kt | 78 ++++++++++++++++-- .../navigation/GoogleMapsNavigationView.kt | 82 ++++++++++++------- 4 files changed, 174 insertions(+), 54 deletions(-) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt index 8f177a23..c00b7b07 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt @@ -40,8 +40,8 @@ internal constructor( // Call all of these three lifecycle functions in sequence to fully // initialize the map view. _mapView.onCreate(context.applicationInfo.metaData) - _mapView.onStart() - _mapView.onResume() + onStart() + onResume() viewRegistry.registerMapView(viewId, this) @@ -55,28 +55,49 @@ internal constructor( } override fun dispose() { + if (super.isDestroyed()) { + return + } + + viewRegistry.unregisterNavigationView(getViewId()) + // When view is disposed, all of these lifecycle functions must be // called to properly dispose navigation view and prevent leaks. - _mapView.onPause() - _mapView.onStop() + onPause() + onStop() + super.onDispose() _mapView.onDestroy() - - viewRegistry.unregisterMapView(getViewId()) } - override fun onStart() { - _mapView.onStart() + override fun onStart():Boolean { + if (super.onStart()) { + _mapView.onStart() + return true + } + return false } - override fun onResume() { - _mapView.onResume() + override fun onResume():Boolean { + if (super.onResume()) { + _mapView.onResume() + return true + } + return false } - override fun onStop() { - _mapView.onStop() + override fun onStop():Boolean { + if (super.onStop()) { + _mapView.onStop() + return true + } + return false } - override fun onPause() { - _mapView.onPause() + override fun onPause():Boolean { + if (super.onPause()) { + _mapView.onPause() + return true + } + return false } } diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt index fe77e96b..6ebf0a31 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt @@ -41,14 +41,19 @@ internal constructor( } // Handled by AndroidAutoBaseScreen. - override fun onStart() {} + override fun onStart():Boolean { + return super.onStart() + } - // Handled by AndroidAutoBaseScreen. - override fun onResume() {} + override fun onResume():Boolean { + return super.onResume() + } - // Handled by AndroidAutoBaseScreen. - override fun onStop() {} + override fun onStop():Boolean { + return super.onStop() + } - // Handled by AndroidAutoBaseScreen. - override fun onPause() {} + override fun onPause():Boolean { + return super.onPause() + } } diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt index 42f8fa27..9d6cac9b 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt @@ -61,18 +61,59 @@ abstract class GoogleMapsBaseMapView( private var _mapReadyCallback: ((Result) -> Unit)? = null private var _pendingCameraEventsListenerSetup = false - /// Default values for UI features. + // Default values for UI features. private var _consumeMyLocationButtonClickEventsEnabled: Boolean = false + // View lifecycle states. + private enum class LifecycleState { + NONE, + STARTED, + RESUMED, + PAUSED, + STOPPED, + DESTROYED + } + // Current lifecycle state tracks the state of the view. + // This is used to avoid calling lifecycle methods in the wrong order. + private var currentLifecycleState: LifecycleState = LifecycleState.NONE + abstract fun getView(): View - abstract fun onStart() + open fun onStart(): Boolean { + if (currentLifecycleState == LifecycleState.STOPPED || currentLifecycleState == LifecycleState.NONE) { + currentLifecycleState = LifecycleState.STARTED + return true + } + return false + } - abstract fun onResume() + open fun onResume(): Boolean { + if (currentLifecycleState == LifecycleState.STARTED || currentLifecycleState == LifecycleState.PAUSED) { + currentLifecycleState = LifecycleState.RESUMED + return true + } + return false + } - abstract fun onStop() + open fun onStop(): Boolean { + if (currentLifecycleState == LifecycleState.PAUSED || currentLifecycleState == LifecycleState.STARTED) { + currentLifecycleState = LifecycleState.STOPPED + return true + } + return false + } - abstract fun onPause() + open fun onPause(): Boolean { + if (currentLifecycleState == LifecycleState.RESUMED) { + currentLifecycleState = LifecycleState.PAUSED + return true + } + return false + } + + protected fun isDestroyed(): Boolean { + return currentLifecycleState == LifecycleState.DESTROYED + } // Method to set the _map object protected fun setMap(map: GoogleMap) { @@ -218,6 +259,33 @@ abstract class GoogleMapsBaseMapView( } } + protected open fun onDispose() { + getMap().run { + setOnMapClickListener(null) + setOnMapLongClickListener(null) + setOnMarkerClickListener(null) + setOnMarkerDragListener(null) + setOnInfoWindowClickListener(null) + setOnInfoWindowClickListener(null) + setOnInfoWindowLongClickListener(null) + setOnPolygonClickListener(null) + setOnPolylineClickListener(null) + setOnMyLocationClickListener(null) + setOnMyLocationButtonClickListener(null) + setOnFollowMyLocationCallback(null) + setOnCameraMoveStartedListener(null) + setOnCameraMoveListener(null) + setOnCameraIdleListener(null) + } + + // Clear surfaceTextureListener + val textureView = findTextureView(getView()) ?: return + textureView.surfaceTextureListener = null; + _map = null; + + currentLifecycleState = LifecycleState.DESTROYED + } + // Installs a custom invalidator for the map view. private fun installInvalidator() { val textureView = findTextureView(getView()) ?: return diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt index 710b9f35..2af22e25 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration import android.view.View import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.libraries.navigation.NavigationView +import com.google.android.libraries.navigation.OnNavigationUiChangedListener import io.flutter.plugin.platform.PlatformView class GoogleMapsNavigationView @@ -46,6 +47,9 @@ internal constructor( private var _isReportIncidentButtonEnabled: Boolean = true private var _isTrafficPromptsEnabled: Boolean = true + private var _onRecenterButtonClickedListener: NavigationView.OnRecenterButtonClickedListener? = null + private var _onNavigationUIEnabledChanged: OnNavigationUiChangedListener? = null + override fun getView(): View { return _navigationView } @@ -54,8 +58,8 @@ internal constructor( // Call all of these three lifecycle functions in sequence to fully // initialize the navigation view. _navigationView.onCreate(context.applicationInfo.metaData) - _navigationView.onStart() - _navigationView.onResume() + onStart() + onResume() // Initialize navigation view with given navigation view options var navigationViewEnabled = false @@ -91,42 +95,60 @@ internal constructor( } override fun dispose() { - getMap().setOnMapClickListener(null) - getMap().setOnMapLongClickListener(null) - getMap().setOnMarkerClickListener(null) - getMap().setOnMarkerDragListener(null) - getMap().setOnInfoWindowClickListener(null) - getMap().setOnInfoWindowClickListener(null) - getMap().setOnInfoWindowLongClickListener(null) - getMap().setOnPolygonClickListener(null) - getMap().setOnPolylineClickListener(null) + if (super.isDestroyed()) { + return + } + + viewRegistry.unregisterNavigationView(getViewId()) + + // Remove navigation view specific listeners + if (_onRecenterButtonClickedListener != null) { + _navigationView.removeOnRecenterButtonClickedListener(_onRecenterButtonClickedListener) + _onRecenterButtonClickedListener = null + } + if (_onNavigationUIEnabledChanged != null) { + _navigationView.removeOnNavigationUiChangedListener(_onNavigationUIEnabledChanged) + _onNavigationUIEnabledChanged = null + } // When view is disposed, all of these lifecycle functions must be // called to properly dispose navigation view and prevent leaks. - _navigationView.isNavigationUiEnabled = false - _navigationView.onPause() - _navigationView.onStop() + onPause() + onStop() + super.onDispose() _navigationView.onDestroy() - - _navigationView.removeOnRecenterButtonClickedListener {} - - viewRegistry.unregisterNavigationView(getViewId()) } - override fun onStart() { - _navigationView.onStart() + override fun onStart():Boolean { + if (super.onStart()) { + _navigationView.onStart() + return true + } + return false } - override fun onResume() { - _navigationView.onResume() + override fun onResume():Boolean { + if (super.onResume()) { + _navigationView.onResume() + return true + } + return false } - override fun onStop() { - _navigationView.onStop() + override fun onStop():Boolean { + if (super.onStop()) { + _navigationView.onStop() + return true + } + return false } - override fun onPause() { - _navigationView.onPause() + override fun onPause():Boolean { + if (super.onPause()) { + _navigationView.onPause() + return true + } + return false } fun onConfigurationChanged(configuration: Configuration) { @@ -138,12 +160,16 @@ internal constructor( } override fun initListeners() { - _navigationView.addOnRecenterButtonClickedListener { + _onRecenterButtonClickedListener = NavigationView.OnRecenterButtonClickedListener { viewEventApi?.onRecenterButtonClicked(getViewId().toLong()) {} } - _navigationView.addOnNavigationUiChangedListener { + _navigationView.addOnRecenterButtonClickedListener(_onRecenterButtonClickedListener) + + _onNavigationUIEnabledChanged = OnNavigationUiChangedListener { viewEventApi?.onNavigationUIEnabledChanged(getViewId().toLong(), it) {} } + _navigationView.addOnNavigationUiChangedListener(_onNavigationUIEnabledChanged) + super.initListeners() } From 7ddaa2f71a7a6058f75edeb867bd0d530ce7ae23 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Fri, 22 Aug 2025 15:46:20 +0300 Subject: [PATCH 2/2] chore: format kotlin code --- .../maps/flutter/navigation/GoogleMapView.kt | 8 +++---- .../navigation/GoogleMapsAutoMapView.kt | 8 +++---- .../navigation/GoogleMapsBaseMapView.kt | 24 +++++++++++++------ .../navigation/GoogleMapsNavigationView.kt | 18 +++++++------- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt index c00b7b07..cc616bb4 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapView.kt @@ -69,7 +69,7 @@ internal constructor( _mapView.onDestroy() } - override fun onStart():Boolean { + override fun onStart(): Boolean { if (super.onStart()) { _mapView.onStart() return true @@ -77,7 +77,7 @@ internal constructor( return false } - override fun onResume():Boolean { + override fun onResume(): Boolean { if (super.onResume()) { _mapView.onResume() return true @@ -85,7 +85,7 @@ internal constructor( return false } - override fun onStop():Boolean { + override fun onStop(): Boolean { if (super.onStop()) { _mapView.onStop() return true @@ -93,7 +93,7 @@ internal constructor( return false } - override fun onPause():Boolean { + override fun onPause(): Boolean { if (super.onPause()) { _mapView.onPause() return true diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt index 6ebf0a31..c50429db 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsAutoMapView.kt @@ -41,19 +41,19 @@ internal constructor( } // Handled by AndroidAutoBaseScreen. - override fun onStart():Boolean { + override fun onStart(): Boolean { return super.onStart() } - override fun onResume():Boolean { + override fun onResume(): Boolean { return super.onResume() } - override fun onStop():Boolean { + override fun onStop(): Boolean { return super.onStop() } - override fun onPause():Boolean { + override fun onPause(): Boolean { return super.onPause() } } diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt index 9d6cac9b..5f790424 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsBaseMapView.kt @@ -71,8 +71,9 @@ abstract class GoogleMapsBaseMapView( RESUMED, PAUSED, STOPPED, - DESTROYED + DESTROYED, } + // Current lifecycle state tracks the state of the view. // This is used to avoid calling lifecycle methods in the wrong order. private var currentLifecycleState: LifecycleState = LifecycleState.NONE @@ -80,7 +81,10 @@ abstract class GoogleMapsBaseMapView( abstract fun getView(): View open fun onStart(): Boolean { - if (currentLifecycleState == LifecycleState.STOPPED || currentLifecycleState == LifecycleState.NONE) { + if ( + currentLifecycleState == LifecycleState.STOPPED || + currentLifecycleState == LifecycleState.NONE + ) { currentLifecycleState = LifecycleState.STARTED return true } @@ -88,7 +92,10 @@ abstract class GoogleMapsBaseMapView( } open fun onResume(): Boolean { - if (currentLifecycleState == LifecycleState.STARTED || currentLifecycleState == LifecycleState.PAUSED) { + if ( + currentLifecycleState == LifecycleState.STARTED || + currentLifecycleState == LifecycleState.PAUSED + ) { currentLifecycleState = LifecycleState.RESUMED return true } @@ -96,7 +103,10 @@ abstract class GoogleMapsBaseMapView( } open fun onStop(): Boolean { - if (currentLifecycleState == LifecycleState.PAUSED || currentLifecycleState == LifecycleState.STARTED) { + if ( + currentLifecycleState == LifecycleState.PAUSED || + currentLifecycleState == LifecycleState.STARTED + ) { currentLifecycleState = LifecycleState.STOPPED return true } @@ -110,7 +120,7 @@ abstract class GoogleMapsBaseMapView( } return false } - + protected fun isDestroyed(): Boolean { return currentLifecycleState == LifecycleState.DESTROYED } @@ -280,8 +290,8 @@ abstract class GoogleMapsBaseMapView( // Clear surfaceTextureListener val textureView = findTextureView(getView()) ?: return - textureView.surfaceTextureListener = null; - _map = null; + textureView.surfaceTextureListener = null + _map = null currentLifecycleState = LifecycleState.DESTROYED } diff --git a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt index 2af22e25..605c2828 100644 --- a/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt +++ b/android/src/main/kotlin/com/google/maps/flutter/navigation/GoogleMapsNavigationView.kt @@ -47,7 +47,8 @@ internal constructor( private var _isReportIncidentButtonEnabled: Boolean = true private var _isTrafficPromptsEnabled: Boolean = true - private var _onRecenterButtonClickedListener: NavigationView.OnRecenterButtonClickedListener? = null + private var _onRecenterButtonClickedListener: NavigationView.OnRecenterButtonClickedListener? = + null private var _onNavigationUIEnabledChanged: OnNavigationUiChangedListener? = null override fun getView(): View { @@ -119,7 +120,7 @@ internal constructor( _navigationView.onDestroy() } - override fun onStart():Boolean { + override fun onStart(): Boolean { if (super.onStart()) { _navigationView.onStart() return true @@ -127,7 +128,7 @@ internal constructor( return false } - override fun onResume():Boolean { + override fun onResume(): Boolean { if (super.onResume()) { _navigationView.onResume() return true @@ -135,7 +136,7 @@ internal constructor( return false } - override fun onStop():Boolean { + override fun onStop(): Boolean { if (super.onStop()) { _navigationView.onStop() return true @@ -143,7 +144,7 @@ internal constructor( return false } - override fun onPause():Boolean { + override fun onPause(): Boolean { if (super.onPause()) { _navigationView.onPause() return true @@ -160,9 +161,10 @@ internal constructor( } override fun initListeners() { - _onRecenterButtonClickedListener = NavigationView.OnRecenterButtonClickedListener { - viewEventApi?.onRecenterButtonClicked(getViewId().toLong()) {} - } + _onRecenterButtonClickedListener = + NavigationView.OnRecenterButtonClickedListener { + viewEventApi?.onRecenterButtonClicked(getViewId().toLong()) {} + } _navigationView.addOnRecenterButtonClickedListener(_onRecenterButtonClickedListener) _onNavigationUIEnabledChanged = OnNavigationUiChangedListener {