Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[google_maps_flutter] improvements to google maps state handling #8431

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 2.11.0

* Adds map mounted check to google map controller map functions.
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.

## 2.10.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,36 @@ void runTests() {
final String? error = await controller.getStyleError();
expect(error, isNotNull);
});

testWidgets('testMapStateException', (WidgetTester tester) async {
final Completer<GoogleMapController> controllerCompleter =
Completer<GoogleMapController>();

await pumpMap(
tester,
GoogleMap(
initialCameraPosition: kInitialCameraPosition,
onMapCreated: (GoogleMapController controller) {
controllerCompleter.complete(controller);
},
),
);
await tester.pumpAndSettle(const Duration(seconds: 3));
final GoogleMapController controller = await controllerCompleter.future;

final double zoomLevel = await controller.getZoomLevel();
expect(zoomLevel > 0, true);

await tester.pumpWidget(Container());
await tester.pumpAndSettle(const Duration(seconds: 3));

try {
await controller.getZoomLevel();
fail('expected MapStateException');
} on MapStateException catch (e) {
expect(e.message.isNotEmpty, true);
}
});
}

/// Repeatedly checks an asynchronous value against a test condition.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,17 @@ flutter:
uses-material-design: true
assets:
- assets/

# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
{
google_maps_flutter_android:
{
path: ../../../../packages/google_maps_flutter/google_maps_flutter_android,
},
google_maps_flutter_ios:
{
path: ../../../../packages/google_maps_flutter/google_maps_flutter_ios,
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@

part of '../google_maps_flutter.dart';

/// Exception for map state errors when calling a function
/// of the map when it's not mounted.
class MapStateException implements Exception {
/// Creates a new instance of [MapStateException].
MapStateException(this.message);

/// Error message describing the exception.
final String message;

@override
String toString() => 'MapStateException: $message';
}

/// Controller for a single GoogleMap instance running on the host platform.
class GoogleMapController {
GoogleMapController._(
Expand Down Expand Up @@ -181,26 +194,41 @@ class GoogleMapController {
/// in-memory cache of tiles. If you want to cache tiles for longer, you
/// should implement an on-disk cache.
Future<void> clearTileCache(TileOverlayId tileOverlayId) async {
return GoogleMapsFlutterPlatform.instance
.clearTileCache(tileOverlayId, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.clearTileCache(tileOverlayId, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method clearTileCache on an unmounted GoogleMap instance.');
}
}

/// Starts an animated change of the map camera position.
///
/// The returned [Future] completes after the change has been started on the
/// platform side.
Future<void> animateCamera(CameraUpdate cameraUpdate) {
return GoogleMapsFlutterPlatform.instance
.animateCamera(cameraUpdate, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.animateCamera(cameraUpdate, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method animateCamera on an unmounted GoogleMap instance.');
}
}

/// Changes the map camera position.
///
/// The returned [Future] completes after the change has been made on the
/// platform side.
Future<void> moveCamera(CameraUpdate cameraUpdate) {
return GoogleMapsFlutterPlatform.instance
.moveCamera(cameraUpdate, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.moveCamera(cameraUpdate, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method moveCamera on an unmounted GoogleMap instance.');
}
}

/// Sets the styling of the base map.
Expand All @@ -218,18 +246,33 @@ class GoogleMapController {
/// style reference for more information regarding the supported styles.
@Deprecated('Use GoogleMap.style instead.')
Future<void> setMapStyle(String? mapStyle) {
return GoogleMapsFlutterPlatform.instance
.setMapStyle(mapStyle, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.setMapStyle(mapStyle, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method setMapStyle on an unmounted GoogleMap instance.');
}
}

/// Returns the last style error, if any.
Future<String?> getStyleError() {
return GoogleMapsFlutterPlatform.instance.getStyleError(mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance.getStyleError(mapId: mapId);
} else {
throw MapStateException(
'Cannot call method getStyleError on an unmounted GoogleMap instance.');
}
}

/// Return [LatLngBounds] defining the region that is visible in a map.
Future<LatLngBounds> getVisibleRegion() {
return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId);
} else {
throw MapStateException(
'Cannot call method getVisibleRegion on an unmounted GoogleMap instance.');
}
}

/// Return [ScreenCoordinate] of the [LatLng] in the current map view.
Expand All @@ -238,17 +281,27 @@ class GoogleMapController {
/// Screen location is in screen pixels (not display pixels) with respect to the top left corner
/// of the map, not necessarily of the whole screen.
Future<ScreenCoordinate> getScreenCoordinate(LatLng latLng) {
return GoogleMapsFlutterPlatform.instance
.getScreenCoordinate(latLng, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.getScreenCoordinate(latLng, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method getScreenCoordinate on an unmounted GoogleMap instance.');
}
}

/// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view.
///
/// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen
/// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen.
Future<LatLng> getLatLng(ScreenCoordinate screenCoordinate) {
return GoogleMapsFlutterPlatform.instance
.getLatLng(screenCoordinate, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.getLatLng(screenCoordinate, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method getLatLng on an unmounted GoogleMap instance.');
}
}

/// Programmatically show the Info Window for a [Marker].
Expand All @@ -260,8 +313,13 @@ class GoogleMapController {
/// * [hideMarkerInfoWindow] to hide the Info Window.
/// * [isMarkerInfoWindowShown] to check if the Info Window is showing.
Future<void> showMarkerInfoWindow(MarkerId markerId) {
return GoogleMapsFlutterPlatform.instance
.showMarkerInfoWindow(markerId, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.showMarkerInfoWindow(markerId, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method showMarkerInfoWindow on an unmounted GoogleMap instance.');
}
}

/// Programmatically hide the Info Window for a [Marker].
Expand All @@ -272,9 +330,15 @@ class GoogleMapController {
/// * See also:
/// * [showMarkerInfoWindow] to show the Info Window.
/// * [isMarkerInfoWindowShown] to check if the Info Window is showing.
///
Future<void> hideMarkerInfoWindow(MarkerId markerId) {
return GoogleMapsFlutterPlatform.instance
.hideMarkerInfoWindow(markerId, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.hideMarkerInfoWindow(markerId, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method hideMarkerInfoWindow on an unmounted GoogleMap instance.');
}
}

/// Returns `true` when the [InfoWindow] is showing, `false` otherwise.
Expand All @@ -286,18 +350,33 @@ class GoogleMapController {
/// * [showMarkerInfoWindow] to show the Info Window.
/// * [hideMarkerInfoWindow] to hide the Info Window.
Future<bool> isMarkerInfoWindowShown(MarkerId markerId) {
return GoogleMapsFlutterPlatform.instance
.isMarkerInfoWindowShown(markerId, mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance
.isMarkerInfoWindowShown(markerId, mapId: mapId);
} else {
throw MapStateException(
'Cannot call method isMarkerInfoWindowShown on an unmounted GoogleMap instance.');
}
}

/// Returns the current zoom level of the map
Future<double> getZoomLevel() {
return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId);
} else {
throw MapStateException(
'Cannot call method getZoomLevel on an unmounted GoogleMap instance.');
}
}

/// Returns the image bytes of the map
Future<Uint8List?> takeSnapshot() {
return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId);
if (_googleMapState.mounted) {
return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId);
} else {
throw MapStateException(
'Cannot call method takeSnapshot on an unmounted GoogleMap instance.');
}
}

/// Disposes of the platform resources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ class _GoogleMapState extends State<GoogleMap> {
circles: widget.circles,
clusterManagers: widget.clusterManagers,
heatmaps: widget.heatmaps,
tileOverlays: widget.tileOverlays,
),
mapConfiguration: _mapConfiguration,
);
Expand Down Expand Up @@ -469,7 +470,6 @@ class _GoogleMapState extends State<GoogleMap> {
this,
);
_controller.complete(controller);
unawaited(_updateTileOverlays());
Copy link
Author

@illuminati1911 illuminati1911 Jan 22, 2025

Choose a reason for hiding this comment

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

Removing this as it seems to be unnecessary when the missing widget.tileOverlays is added to the google map. Also this may have cause this issue:
flutter/flutter#43785 (comment)

final MapCreatedCallback? onMapCreated = widget.onMapCreated;
if (onMapCreated != null) {
onMapCreated(controller);
Expand Down
14 changes: 13 additions & 1 deletion packages/google_maps_flutter/google_maps_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: google_maps_flutter
description: A Flutter plugin for integrating Google Maps in iOS and Android applications.
repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22
version: 2.10.0
version: 2.11.0

environment:
sdk: ^3.4.0
Expand Down Expand Up @@ -41,3 +41,15 @@ topics:
# The example deliberately includes limited-use secrets.
false_secrets:
- /example/web/index.html

# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE.
# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins
dependency_overrides:
{
google_maps_flutter_android:
{
path: ../../../packages/google_maps_flutter/google_maps_flutter_android,
},
google_maps_flutter_ios:
{ path: ../../../packages/google_maps_flutter/google_maps_flutter_ios },
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
Expand Down Expand Up @@ -579,4 +582,41 @@ void main() {

expect(map.mapConfiguration.style, '');
});

testWidgets('testMapStateException', (WidgetTester tester) async {
final Completer<GoogleMapController> controllerCompleter =
Completer<GoogleMapController>();

await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: GoogleMap(
initialCameraPosition:
const CameraPosition(target: LatLng(10.0, 15.0)),
onMapCreated: (GoogleMapController controller) {
controllerCompleter.complete(controller);
},
),
),
);

await tester.pumpAndSettle(const Duration(seconds: 3));
final GoogleMapController controller = await controllerCompleter.future;

try {
await controller.getZoomLevel();
} on MapStateException {
fail('should not throw MapStateException');
}

await tester.pumpWidget(Container());
await tester.pumpAndSettle(const Duration(seconds: 3));

try {
await controller.getZoomLevel();
fail('expected MapStateException');
} on MapStateException catch (e) {
expect(e.message.isNotEmpty, true);
}
});
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.14.13

* Minor fix for managing tile overlays.

## 2.14.12

* Updates androidx.annotation:annotation to 1.9.1.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {

@override
void dispose({required int mapId}) {
// Noop!
_tileOverlays.remove(mapId);
}

// The controller we need to broadcast the different events coming
Expand Down Expand Up @@ -529,6 +529,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform {
.toList(),
);

if (_tileOverlays[creationId] == null) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the issue this change corresponds to? It's not at all clear to me what this is fixing, or why it is necessary specifically for tile overlays and no other types.

// Initialize the tile overlays for the mapId.
_tileOverlays[creationId] = keyTileOverlayId(mapObjects.tileOverlays);
}

const String viewType = 'plugins.flutter.dev/google_maps_android';
if (useAndroidViewSurface) {
return PlatformViewLink(
Expand Down
Loading
Loading