googlemaps / googlemaps/flutter-navigation-sdk
[Bug]: Android crash when GoogleMapsMapView is disposed before getMapAsync completes
- Dominant language
- Dart
- Stars
- 75
- Forks
- 54
- Avg merge
- 4d 2h
- Merged PRs (30d)
- 5
Description
### Is there an existing issue for this?
- [x] I have searched the existing issues
### Description of the bug
`GoogleMapsMapView` can crash on Android when the platform view is disposed before asynchronous native map initialization completes.
`GoogleMapView` initializes the native map using `MapView.getMapAsync()`. If Flutter disposes the platform view—or Android destroys the `FlutterActivity`—before this callback completes, the internal `_map` reference remains null.
During teardown, `GoogleMapsBaseMapView.onDispose()` calls `getMap()` unconditionally. Because the native map has not been initialized, `getMap()` throws the following fatal exception:
`GoogleMap not initialized yet`
We observed this race condition in seven production sessions using `google_navigation_flutter` version `0.9.4`. The recorded paths include both individual platform-view disposal and full `FlutterEngine`/`FlutterActivity` teardown.
Available telemetry confirms that application startup and Flutter's first frame completed successfully before the crashes. Therefore, this appears to be an intermittent native platform-view lifecycle race rather than a general application startup or permanent device initialization failure.
### iOS Platform
Not verified
### Android Platform
Affected
### Flutter version
3.44.2
### Package version
0.9.4
### Native SDK versions
- [x] I haven't changed the version of the native SDKs
### Flutter Doctor Output
[✓] Flutter (Channel stable, 3.44.2, on macOS 26.6.2 darwin-arm64, locale en-US)
• Flutter version 3.44.2 on channel stable
• Framework revision c9a6c48423
• Engine revision 77e2e94772
• Dart version 3.12.2
• DevTools version 2.57.0
• Swift Package Manager disabled
[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0)
• Platform android-36, build-tools 36.1.0
• Java version OpenJDK Runtime Environment 21.0.8
• All Android licenses accepted
[✓] Xcode - develop for iOS and macOS (Xcode 26.4)
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web
[✓] Connected devices available
• Android arm64 device running Android 15 (API 35)
• iOS devices
• macOS
• Chrome
[✓] Network resources
• All expected network resources are available.
• No issues found!
### Steps to reproduce
The race is intermittent and has primarily been observed through production crash reports.
1. Configure the Android API key according to the package documentation.
2. Run an Android application containing a standalone `GoogleMapsMapView`.
3. Open the screen containing the map.
4. Dispose the map platform view immediately, before `MapView.getMapAsync()` completes. This can be triggered by:
- navigating away from the screen quickly;
- repeatedly creating and removing the map widget; or
- destroying the `FlutterActivity` while the map is loading.
5. Observe the intermittent fatal exception from `GoogleMapsBaseMapView.onDispose()`.
Slower map initialization or higher system load may make the timing window easier to reproduce.
We observed seven production occurrences across two application versions. The stack traces include both individual platform-view disposal and complete `FlutterEngine`/`FlutterActivity` teardown.
### Expected vs Actual Behavior
## Expected behaviour
`GoogleMapsMapView` should be safely disposable at any point in its lifecycle, including while `MapView.getMapAsync()` is still pending.
If the map is not initialized:
- listener cleanup should be skipped safely;
- the platform view should still transition to the destroyed state; and
- a late `getMapAsync()` callback should be ignored.
## Actual behaviour
`GoogleMapsBaseMapView.onDispose()` calls `getMap()` unconditionally. When the asynchronous map initialization has not completed, the internal `_map` reference is null and `getMap()` throws:
`GoogleMap not initialized yet`
This becomes a fatal Android exception:
```text
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
Caused by: com.google.maps.flutter.navigation.FlutterError:
GoogleMap not initialized yet
at com.google.maps.flutter.navigation.GoogleMapsBaseMapView.getMap
at com.google.maps.flutter.navigation.GoogleMapsBaseMapView.onDispose
at com.google.maps.flutter.navigation.GoogleMapView.dispose
at io.flutter.plugin.platform.PlatformViewsController$1.dispose
For activity-teardown occurrences, the stack continues through:
PlatformViewsController.disposeAllViews
PlatformViewsController.onDetachedFromJNI
FlutterEngine.destroy
FlutterActivityAndFragmentDelegate.onDetach
FlutterActivity.onDestroy
```
### Code Sample
```dart
import 'package:flutter/material.dart';
import 'package:google_navigation_flutter/google_navigation_flutter.dart';
void main() {
runApp(const MaterialApp(home: MapDisposeReproduction()));
}
class MapDisposeReproduction extends StatefulWidget {
const MapDisposeReproduction({super.key});
@override
State createState() =>
_MapDisposeReproductionState();
}
class _MapDisposeReproductionState
extends State {
bool _showMap = false;
int _mapGeneration = 0;
Future _runStressTest() async {
for (var index = 0; index < 20; index++) {
if (!mounted) return;
setState(() {
_mapGeneration++;
_showMap = true;
});
// Attempt to dispose the platform view while getMapAsync() is pending.
await Future.delayed(const Duration(milliseconds: 50));
if (!mounted) return;
setState(() => _showMap = false);
await Future.delayed(const Duration(milliseconds: 50));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Map disposal reproduction')),
body: Column(
children: [
Expanded(
child: _showMap
? GoogleMapsMapView(
key: ValueKey(_mapGeneration),
onViewCreated: (_) {},
)
: const Center(child: Text('Map disposed')),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _runStressTest,
child: const Text('Run disposal stress test'),
),
),
],
),
);
}
}
```
### Additional Context
The failure appears to be caused by a race between asynchronous native map initialization and platform-view disposal.
`GoogleMapView` starts initialization through:
```kotlin
_mapView.getMapAsync { map ->
setMap(map)
initListeners()
mapReady()
}
```
However, `GoogleMapsBaseMapView.onDispose()` currently calls:
```kotlin
getMap().run {
// Unregister listeners.
}
```
`getMap()` throws when `_map` is still null.
## Proposed fix
1. Make listener cleanup null-safe:
```kotlin
_map?.run {
// Unregister listeners.
}
```
2. Do not return early when no `TextureView` is found. The platform view must always clear its references and transition to `DESTROYED`:
```kotlin
findTextureView(getView())?.surfaceTextureListener = null
_map = null
_indoorStateChangeListener = null
currentLifecycleState = LifecycleState.DESTROYED
```
3. Ignore asynchronous map-ready callbacks that arrive after disposal:
```kotlin
_mapView.getMapAsync { map ->
if (isDestroyed()) return@getMapAsync
setMap(map)
initListeners()
imageRegistry.mapViewInitializationComplete()
mapReady()
}
```
The late-callback guard should also be applied to `GoogleMapsNavigationView` for lifecycle consistency, although the observed stack traces specifically involve the standalone `GoogleMapView`.
## Suggested regression tests
- Dispose before `getMapAsync()` completes.
- Deliver the map-ready callback after disposal.
- Dispose the same view more than once.
- Verify the normal map-ready-then-dispose path remains unchanged.
Related issues were reviewed:
- #274 reports a different failure in `GoogleMapsNavigationSessionManager`.
- #473 reports an iOS `viewNotFound` error.
Crash-report identifiers and application-specific user/device information have been omitted.
Contributor guide
Assessment
This issue has not been assessed yet.