diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index c14478638..1622ffe9c 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.2.0 + +* Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, + replacing direct `WebViewController` calls. +* Remove the public `GoogleMapsController.controller` field. +* Update `webview_flutter_lwe` to ^0.5.3. +* Raise the SDK constraint to Dart ^3.8.0 and Flutter >=3.32.0. +* Fix a JS string escaping bug that could break marker and overlay creation. +* Fix event handling and dispose-safety issues in the JS bridge. +* Fix a null-check bug in circle tap handling. +* Verify integration tests pass against upstream google_maps_flutter v2.17.0. + ## 0.1.14 * Update google_maps_flutter to 2.16.0. diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index 0cbdb4161..67e3411d7 100644 --- a/packages/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/README.md @@ -21,7 +21,7 @@ This package is not an _endorsed_ implementation of `google_maps_flutter`. There ```yaml dependencies: google_maps_flutter: ^2.16.0 - google_maps_flutter_tizen: ^0.1.14 + google_maps_flutter_tizen: ^0.2.0 ``` For detailed usage, see https://pub.dev/packages/google_maps_flutter#sample-usage. diff --git a/packages/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/example/integration_test/google_maps_test.dart index eaa901f5f..097cbf752 100644 --- a/packages/google_maps_flutter/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -5,13 +5,14 @@ import 'package:integration_test/integration_test.dart'; import 'src/maps_controller.dart' as maps_controller; + // import 'src/maps_inspector.dart' as maps_inspector; // import 'src/tiles_inspector.dart' as tiles_inspector; /// Recombine all test files in `src` into a single test app. /// -/// This is done to ensure that all the integration tests run in the same FTL app, -/// rather than spinning multiple different tasks. +/// This is done to ensure that all the integration tests run in the same FTL +/// app, rather than spinning multiple different tasks. void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/google_maps_flutter/example/integration_test/src/maps_controller.dart b/packages/google_maps_flutter/example/integration_test/src/maps_controller.dart index 40f911232..603e819c4 100644 --- a/packages/google_maps_flutter/example/integration_test/src/maps_controller.dart +++ b/packages/google_maps_flutter/example/integration_test/src/maps_controller.dart @@ -106,11 +106,11 @@ void runTests() { // Wait for the visible region to be non-zero. final LatLngBounds firstVisibleRegion = await waitForValueMatchingPredicate( - tester, - () => mapController.getVisibleRegion(), - (LatLngBounds bounds) => bounds != zeroLatLngBounds, - ) ?? - zeroLatLngBounds; + tester, + () => mapController.getVisibleRegion(), + (LatLngBounds bounds) => bounds != zeroLatLngBounds, + ) ?? + zeroLatLngBounds; expect(firstVisibleRegion, isNot(zeroLatLngBounds)); expect(firstVisibleRegion.contains(kInitialMapCenter), isTrue); @@ -145,8 +145,8 @@ void runTests() { ); await tester.pumpAndSettle(const Duration(seconds: 3)); - final LatLngBounds secondVisibleRegion = - await mapController.getVisibleRegion(); + final LatLngBounds secondVisibleRegion = await mapController + .getVisibleRegion(); expect(secondVisibleRegion, isNot(zeroLatLngBounds)); @@ -422,7 +422,8 @@ void runTests() { await controller.showMarkerInfoWindow(marker.markerId); // The Maps SDK doesn't always return true for whether it is shown // immediately after showing it, so wait for it to report as shown. - iwVisibleStatus = await waitForValueMatchingPredicate( + iwVisibleStatus = + await waitForValueMatchingPredicate( tester, () => controller.isMarkerInfoWindowShown(marker.markerId), (bool visible) => visible, diff --git a/packages/google_maps_flutter/example/integration_test/src/maps_inspector.dart b/packages/google_maps_flutter/example/integration_test/src/maps_inspector.dart index 90dea1b0b..8275e9311 100644 --- a/packages/google_maps_flutter/example/integration_test/src/maps_inspector.dart +++ b/packages/google_maps_flutter/example/integration_test/src/maps_inspector.dart @@ -127,8 +127,8 @@ void runTests() { final GoogleMapController controller = await controllerCompleter.future; if (isIOS) { - final MinMaxZoomPreference zoomLevel = - await inspector.getMinMaxZoomLevels(mapId: controller.mapId); + final MinMaxZoomPreference zoomLevel = await inspector + .getMinMaxZoomLevels(mapId: controller.mapId); expect(zoomLevel, equals(initialZoomLevel)); } else if (isAndroid) { await controller.moveCamera(CameraUpdate.zoomTo(15)); @@ -155,8 +155,8 @@ void runTests() { ); if (isIOS) { - final MinMaxZoomPreference zoomLevel = - await inspector.getMinMaxZoomLevels(mapId: controller.mapId); + final MinMaxZoomPreference zoomLevel = await inspector + .getMinMaxZoomLevels(mapId: controller.mapId); expect(zoomLevel, equals(finalZoomLevel)); } else { await controller.moveCamera(CameraUpdate.zoomTo(15)); @@ -518,8 +518,8 @@ void runTests() { ); final int mapId = await mapIdCompleter.future; - final bool myLocationButtonEnabled = - await inspector.isMyLocationButtonEnabled(mapId: mapId); + final bool myLocationButtonEnabled = await inspector + .isMyLocationButtonEnabled(mapId: mapId); expect(myLocationButtonEnabled, false); }); @@ -541,8 +541,8 @@ void runTests() { ); final int mapId = await mapIdCompleter.future; - final bool myLocationButtonEnabled = - await inspector.isMyLocationButtonEnabled(mapId: mapId); + final bool myLocationButtonEnabled = await inspector + .isMyLocationButtonEnabled(mapId: mapId); expect(myLocationButtonEnabled, true); }); }, skip: !isIOS); diff --git a/packages/google_maps_flutter/example/integration_test/src/shared.dart b/packages/google_maps_flutter/example/integration_test/src/shared.dart index 3b91ae828..047072027 100644 --- a/packages/google_maps_flutter/example/integration_test/src/shared.dart +++ b/packages/google_maps_flutter/example/integration_test/src/shared.dart @@ -50,7 +50,9 @@ Future pumpMap( Widget wrapMap(GoogleMap map, [Size size = const Size.square(200)]) { return MaterialApp( home: Scaffold( - body: Center(child: SizedBox.fromSize(size: size, child: map)), + body: Center( + child: SizedBox.fromSize(size: size, child: map), + ), ), ); } diff --git a/packages/google_maps_flutter/example/integration_test/src/tiles_inspector.dart b/packages/google_maps_flutter/example/integration_test/src/tiles_inspector.dart index a3b6176a5..ef937e865 100644 --- a/packages/google_maps_flutter/example/integration_test/src/tiles_inspector.dart +++ b/packages/google_maps_flutter/example/integration_test/src/tiles_inspector.dart @@ -351,10 +351,14 @@ void runTests() { GoogleMapsInspectorPlatform.instance!; if (inspector.supportsGettingHeatmapInfo()) { - final Heatmap heatmapInfo1 = - (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; - final Heatmap heatmapInfo2 = - (await inspector.getHeatmapInfo(heatmap2.mapsId, mapId: mapId))!; + final Heatmap heatmapInfo1 = (await inspector.getHeatmapInfo( + heatmap1.mapsId, + mapId: mapId, + ))!; + final Heatmap heatmapInfo2 = (await inspector.getHeatmapInfo( + heatmap2.mapsId, + mapId: mapId, + ))!; expectHeatmapEquals(heatmap1, heatmapInfo1); expectHeatmapEquals(heatmap2, heatmapInfo2); @@ -411,8 +415,10 @@ void runTests() { await tester.pumpAndSettle(const Duration(seconds: 3)); if (inspector.supportsGettingHeatmapInfo()) { - final Heatmap heatmapInfo1 = - (await inspector.getHeatmapInfo(heatmap1.mapsId, mapId: mapId))!; + final Heatmap heatmapInfo1 = (await inspector.getHeatmapInfo( + heatmap1.mapsId, + mapId: mapId, + ))!; expectHeatmapEquals(heatmap1New, heatmapInfo1); } diff --git a/packages/google_maps_flutter/example/lib/animate_camera.dart b/packages/google_maps_flutter/example/lib/animate_camera.dart index 8e3e2a55a..4496c5a45 100644 --- a/packages/google_maps_flutter/example/lib/animate_camera.dart +++ b/packages/google_maps_flutter/example/lib/animate_camera.dart @@ -11,7 +11,7 @@ import 'page.dart'; class AnimateCameraPage extends GoogleMapExampleAppPage { const AnimateCameraPage({Key? key}) - : super(const Icon(Icons.map), 'Camera control, animated', key: key); + : super(const Icon(Icons.map), 'Camera control, animated', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/clustering.dart b/packages/google_maps_flutter/example/lib/clustering.dart index 37bdc08ac..0445ba57b 100644 --- a/packages/google_maps_flutter/example/lib/clustering.dart +++ b/packages/google_maps_flutter/example/lib/clustering.dart @@ -13,7 +13,7 @@ import 'page.dart'; class ClusteringPage extends GoogleMapExampleAppPage { /// Default Constructor. const ClusteringPage({Key? key}) - : super(const Icon(Icons.place), 'Manage clustering', key: key); + : super(const Icon(Icons.place), 'Manage clustering', key: key); @override Widget build(BuildContext context) { @@ -156,8 +156,8 @@ class ClusteringBodyState extends State { final MarkerId markerId = MarkerId(markerIdVal); final int clusterManagerIndex = clusterManagers.values.toList().indexOf( - clusterManager, - ); + clusterManager, + ); // Add additional offset to longitude for each cluster manager to space // out markers in different cluster managers. @@ -236,9 +236,8 @@ class ClusteringBodyState extends State { TextButton( onPressed: clusterManagers.isEmpty ? null - : () => _removeClusterManager( - clusterManagers.values.last, - ), + : () => + _removeClusterManager(clusterManagers.values.last), child: const Text('Remove cluster manager'), ), ], @@ -246,8 +245,9 @@ class ClusteringBodyState extends State { Wrap( alignment: WrapAlignment.spaceEvenly, children: [ - for (final MapEntry clusterEntry in clusterManagers.entries) + for (final MapEntry + clusterEntry + in clusterManagers.entries) TextButton( onPressed: () => _addMarkersToCluster(clusterEntry.value), child: Text('Add markers to ${clusterEntry.key.value}'), @@ -269,8 +269,9 @@ class ClusteringBodyState extends State { child: const Text('Remove selected marker'), ), TextButton( - onPressed: - markers.isEmpty ? null : () => _changeMarkersAlpha(), + onPressed: markers.isEmpty + ? null + : () => _changeMarkersAlpha(), child: const Text('Change all markers alpha'), ), ], diff --git a/packages/google_maps_flutter/example/lib/custom_marker_icon.dart b/packages/google_maps_flutter/example/lib/custom_marker_icon.dart index bd46858f6..02daad7a5 100644 --- a/packages/google_maps_flutter/example/lib/custom_marker_icon.dart +++ b/packages/google_maps_flutter/example/lib/custom_marker_icon.dart @@ -16,9 +16,9 @@ Future createCustomMarkerIconImage({required Size size}) async { painter.paint(canvas, size); final ui.Image image = await recorder.endRecording().toImage( - size.width.floor(), - size.height.floor(), - ); + size.width.floor(), + size.height.floor(), + ); final ByteData? bytes = await image.toByteData( format: ui.ImageByteFormat.png, diff --git a/packages/google_maps_flutter/example/lib/ground_overlay.dart b/packages/google_maps_flutter/example/lib/ground_overlay.dart index 8176ea392..adb98777c 100644 --- a/packages/google_maps_flutter/example/lib/ground_overlay.dart +++ b/packages/google_maps_flutter/example/lib/ground_overlay.dart @@ -11,7 +11,7 @@ import 'page.dart'; class GroundOverlayPage extends GoogleMapExampleAppPage { const GroundOverlayPage({Key? key}) - : super(const Icon(Icons.map), 'Ground overlay', key: key); + : super(const Icon(Icons.map), 'Ground overlay', key: key); @override Widget build(BuildContext context) { @@ -81,10 +81,12 @@ class GroundOverlayBodyState extends State { return; } setState(() { - final double transparency = - _groundOverlay!.transparency == 0.0 ? 0.5 : 0.0; - _groundOverlay = - _groundOverlay!.copyWith(transparencyParam: transparency); + final double transparency = _groundOverlay!.transparency == 0.0 + ? 0.5 + : 0.0; + _groundOverlay = _groundOverlay!.copyWith( + transparencyParam: transparency, + ); }); } @@ -93,8 +95,9 @@ class GroundOverlayBodyState extends State { return; } setState(() { - _groundOverlay = - _groundOverlay!.copyWith(visibleParam: !_groundOverlay!.visible); + _groundOverlay = _groundOverlay!.copyWith( + visibleParam: !_groundOverlay!.visible, + ); }); } diff --git a/packages/google_maps_flutter/example/lib/heatmap.dart b/packages/google_maps_flutter/example/lib/heatmap.dart index b2404f66f..d8f166cd6 100644 --- a/packages/google_maps_flutter/example/lib/heatmap.dart +++ b/packages/google_maps_flutter/example/lib/heatmap.dart @@ -11,7 +11,7 @@ import 'page.dart'; class HeatmapPage extends GoogleMapExampleAppPage { const HeatmapPage({Key? key}) - : super(const Icon(Icons.map), 'Heatmaps', key: key); + : super(const Icon(Icons.map), 'Heatmaps', key: key); @override Widget build(BuildContext context) { @@ -127,13 +127,15 @@ class HeatmapBodyState extends State { Column( children: [ TextButton( - onPressed: - disabledPoints.isNotEmpty ? _addPoint : null, + onPressed: disabledPoints.isNotEmpty + ? _addPoint + : null, child: const Text('Add point'), ), TextButton( - onPressed: - enabledPoints.isNotEmpty ? _removePoint : null, + onPressed: enabledPoints.isNotEmpty + ? _removePoint + : null, child: const Text('Remove point'), ), ], diff --git a/packages/google_maps_flutter/example/lib/lite_mode.dart b/packages/google_maps_flutter/example/lib/lite_mode.dart index 6be5eea8b..96c1c8434 100644 --- a/packages/google_maps_flutter/example/lib/lite_mode.dart +++ b/packages/google_maps_flutter/example/lib/lite_mode.dart @@ -15,7 +15,7 @@ const CameraPosition _kInitialPosition = CameraPosition( class LiteModePage extends GoogleMapExampleAppPage { const LiteModePage({Key? key}) - : super(const Icon(Icons.map), 'Lite mode', key: key); + : super(const Icon(Icons.map), 'Lite mode', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/example/lib/main.dart index 2a3effab2..c500cb346 100644 --- a/packages/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/example/lib/main.dart @@ -55,8 +55,10 @@ class MapsDemo extends StatelessWidget { void _pushPage(BuildContext context, GoogleMapExampleAppPage page) { Navigator.of(context).push( MaterialPageRoute( - builder: (_) => - Scaffold(appBar: AppBar(title: Text(page.title)), body: page), + builder: (_) => Scaffold( + appBar: AppBar(title: Text(page.title)), + body: page, + ), ), ); } diff --git a/packages/google_maps_flutter/example/lib/map_click.dart b/packages/google_maps_flutter/example/lib/map_click.dart index dc6b5af8e..f48ca5414 100644 --- a/packages/google_maps_flutter/example/lib/map_click.dart +++ b/packages/google_maps_flutter/example/lib/map_click.dart @@ -15,7 +15,7 @@ const CameraPosition _kInitialPosition = CameraPosition( class MapClickPage extends GoogleMapExampleAppPage { const MapClickPage({Key? key}) - : super(const Icon(Icons.mouse), 'Map click', key: key); + : super(const Icon(Icons.mouse), 'Map click', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/map_coordinates.dart b/packages/google_maps_flutter/example/lib/map_coordinates.dart index 8f7e26f8b..ab8bb7703 100644 --- a/packages/google_maps_flutter/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/example/lib/map_coordinates.dart @@ -15,7 +15,7 @@ const CameraPosition _kInitialPosition = CameraPosition( class MapCoordinatesPage extends GoogleMapExampleAppPage { const MapCoordinatesPage({Key? key}) - : super(const Icon(Icons.map), 'Map coordinates', key: key); + : super(const Icon(Icons.map), 'Map coordinates', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/map_map_id.dart b/packages/google_maps_flutter/example/lib/map_map_id.dart index 859be988f..3ca942223 100644 --- a/packages/google_maps_flutter/example/lib/map_map_id.dart +++ b/packages/google_maps_flutter/example/lib/map_map_id.dart @@ -9,7 +9,7 @@ import 'page.dart'; class MapIdPage extends GoogleMapExampleAppPage { const MapIdPage({Key? key}) - : super(const Icon(Icons.map), 'Cloud-based maps styling', key: key); + : super(const Icon(Icons.map), 'Cloud-based maps styling', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/example/lib/map_ui.dart index 1455fe7c6..f94d9abb8 100644 --- a/packages/google_maps_flutter/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/example/lib/map_ui.dart @@ -18,7 +18,7 @@ final LatLngBounds sydneyBounds = LatLngBounds( class MapUiPage extends GoogleMapExampleAppPage { const MapUiPage({Key? key}) - : super(const Icon(Icons.map), 'User interface', key: key); + : super(const Icon(Icons.map), 'User interface', key: key); @override Widget build(BuildContext context) { @@ -255,8 +255,9 @@ class MapUiBodyState extends State { child: Text('${_nightMode ? 'disable' : 'enable'} night mode'), onPressed: () async { _nightMode = !_nightMode; - final String style = - _nightMode ? await _getFileData('assets/night_mode.json') : ''; + final String style = _nightMode + ? await _getFileData('assets/night_mode.json') + : ''; setState(() { _mapStyle = style; }); diff --git a/packages/google_maps_flutter/example/lib/marker_icons.dart b/packages/google_maps_flutter/example/lib/marker_icons.dart index a8226d515..1b17bcf98 100644 --- a/packages/google_maps_flutter/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/example/lib/marker_icons.dart @@ -16,7 +16,7 @@ import 'page.dart'; class MarkerIconsPage extends GoogleMapExampleAppPage { const MarkerIconsPage({Key? key}) - : super(const Icon(Icons.image), 'Marker icons', key: key); + : super(const Icon(Icons.image), 'Marker icons', key: key); @override Widget build(BuildContext context) { @@ -251,10 +251,12 @@ class MarkerIconsBodyState extends State { Future _updateMarkerAssetImage(BuildContext context) async { // Width and height are used only for custom size. - final (double? width, double? height) = - _scalingEnabled && _customSizeEnabled - ? _getCurrentMarkerSize() - : (null, null); + final ( + double? width, + double? height, + ) = _scalingEnabled && _customSizeEnabled + ? _getCurrentMarkerSize() + : (null, null); AssetMapBitmap assetMapBitmap; if (_mipMapsEnabled) { @@ -266,8 +268,9 @@ class MarkerIconsBodyState extends State { 'assets/red_square.png', width: width, height: height, - bitmapScaling: - _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none, + bitmapScaling: _scalingEnabled + ? MapBitmapScaling.auto + : MapBitmapScaling.none, ); } else { // Uses hardcoded asset path @@ -277,8 +280,9 @@ class MarkerIconsBodyState extends State { 'assets/red_square.png', width: width, height: height, - bitmapScaling: - _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none, + bitmapScaling: _scalingEnabled + ? MapBitmapScaling.auto + : MapBitmapScaling.none, ); } @@ -302,18 +306,21 @@ class MarkerIconsBodyState extends State { final ByteData bytes = await createCustomMarkerIconImage(size: canvasSize); // Width and height are used only for custom size. - final (double? width, double? height) = - _scalingEnabled && _customSizeEnabled - ? _getCurrentMarkerSize() - : (null, null); + final ( + double? width, + double? height, + ) = _scalingEnabled && _customSizeEnabled + ? _getCurrentMarkerSize() + : (null, null); final BytesMapBitmap bitmap = BytesMapBitmap( bytes.buffer.asUint8List(), imagePixelRatio: imagePixelRatio, width: width, height: height, - bitmapScaling: - _scalingEnabled ? MapBitmapScaling.auto : MapBitmapScaling.none, + bitmapScaling: _scalingEnabled + ? MapBitmapScaling.auto + : MapBitmapScaling.none, ); _updateBytesBitmap(bitmap); diff --git a/packages/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/example/lib/move_camera.dart index 87face1d0..460dc00ee 100644 --- a/packages/google_maps_flutter/example/lib/move_camera.dart +++ b/packages/google_maps_flutter/example/lib/move_camera.dart @@ -11,7 +11,7 @@ import 'page.dart'; class MoveCameraPage extends GoogleMapExampleAppPage { const MoveCameraPage({Key? key}) - : super(const Icon(Icons.map), 'Camera control', key: key); + : super(const Icon(Icons.map), 'Camera control', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/padding.dart b/packages/google_maps_flutter/example/lib/padding.dart index 0dedd0da7..0ae39ec24 100644 --- a/packages/google_maps_flutter/example/lib/padding.dart +++ b/packages/google_maps_flutter/example/lib/padding.dart @@ -10,7 +10,7 @@ import 'page.dart'; class PaddingPage extends GoogleMapExampleAppPage { const PaddingPage({Key? key}) - : super(const Icon(Icons.map), 'Add padding to the map', key: key); + : super(const Icon(Icons.map), 'Add padding to the map', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/place_circle.dart b/packages/google_maps_flutter/example/lib/place_circle.dart index 086d9b1f8..c39f97c89 100644 --- a/packages/google_maps_flutter/example/lib/place_circle.dart +++ b/packages/google_maps_flutter/example/lib/place_circle.dart @@ -11,7 +11,7 @@ import 'page.dart'; class PlaceCirclePage extends GoogleMapExampleAppPage { const PlaceCirclePage({Key? key}) - : super(const Icon(Icons.linear_scale), 'Place circle', key: key); + : super(const Icon(Icons.linear_scale), 'Place circle', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/example/lib/place_marker.dart index 5964e96fb..85c972327 100644 --- a/packages/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/example/lib/place_marker.dart @@ -16,7 +16,7 @@ import 'page.dart'; class PlaceMarkerPage extends GoogleMapExampleAppPage { const PlaceMarkerPage({Key? key}) - : super(const Icon(Icons.place), 'Place marker', key: key); + : super(const Icon(Icons.place), 'Place marker', key: key); @override Widget build(BuildContext context) { @@ -286,8 +286,9 @@ class PlaceMarkerBodyState extends State { children: [ TextButton(onPressed: _add, child: const Text('Add')), TextButton( - onPressed: - selectedId == null ? null : () => _remove(selectedId), + onPressed: selectedId == null + ? null + : () => _remove(selectedId), child: const Text('Remove'), ), ], @@ -296,8 +297,9 @@ class PlaceMarkerBodyState extends State { alignment: WrapAlignment.spaceEvenly, children: [ TextButton( - onPressed: - selectedId == null ? null : () => _changeInfo(selectedId), + onPressed: selectedId == null + ? null + : () => _changeInfo(selectedId), child: const Text('change info'), ), TextButton( @@ -325,8 +327,9 @@ class PlaceMarkerBodyState extends State { child: const Text('toggle draggable'), ), TextButton( - onPressed: - selectedId == null ? null : () => _toggleFlat(selectedId), + onPressed: selectedId == null + ? null + : () => _toggleFlat(selectedId), child: const Text('toggle flat'), ), TextButton( @@ -357,9 +360,7 @@ class PlaceMarkerBodyState extends State { onPressed: selectedId == null ? null : () { - _getMarkerIcon(context).then(( - BitmapDescriptor icon, - ) { + _getMarkerIcon(context).then((BitmapDescriptor icon) { _setMarkerIcon(selectedId, icon); }); }, diff --git a/packages/google_maps_flutter/example/lib/place_polygon.dart b/packages/google_maps_flutter/example/lib/place_polygon.dart index abee628ab..8d5b412be 100644 --- a/packages/google_maps_flutter/example/lib/place_polygon.dart +++ b/packages/google_maps_flutter/example/lib/place_polygon.dart @@ -11,7 +11,7 @@ import 'page.dart'; class PlacePolygonPage extends GoogleMapExampleAppPage { const PlacePolygonPage({Key? key}) - : super(const Icon(Icons.linear_scale), 'Place polygon', key: key); + : super(const Icon(Icons.linear_scale), 'Place polygon', key: key); @override Widget build(BuildContext context) { @@ -218,16 +218,16 @@ class PlacePolygonBodyState extends State { onPressed: (selectedId == null) ? null : (polygons[selectedId]!.holes.isNotEmpty - ? null - : () => _addHoles(selectedId)), + ? null + : () => _addHoles(selectedId)), child: const Text('add holes'), ), TextButton( onPressed: (selectedId == null) ? null : (polygons[selectedId]!.holes.isEmpty - ? null - : () => _removeHoles(selectedId)), + ? null + : () => _removeHoles(selectedId)), child: const Text('remove holes'), ), TextButton( diff --git a/packages/google_maps_flutter/example/lib/place_polyline.dart b/packages/google_maps_flutter/example/lib/place_polyline.dart index 47285b151..fb68a97a2 100644 --- a/packages/google_maps_flutter/example/lib/place_polyline.dart +++ b/packages/google_maps_flutter/example/lib/place_polyline.dart @@ -12,7 +12,7 @@ import 'page.dart'; class PlacePolylinePage extends GoogleMapExampleAppPage { const PlacePolylinePage({Key? key}) - : super(const Icon(Icons.linear_scale), 'Place polyline', key: key); + : super(const Icon(Icons.linear_scale), 'Place polyline', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/example/lib/scrolling_map.dart index 66b6ccfa9..17c610eb4 100644 --- a/packages/google_maps_flutter/example/lib/scrolling_map.dart +++ b/packages/google_maps_flutter/example/lib/scrolling_map.dart @@ -15,7 +15,7 @@ const LatLng _center = LatLng(32.080664, 34.9563837); class ScrollingMapPage extends GoogleMapExampleAppPage { const ScrollingMapPage({Key? key}) - : super(const Icon(Icons.map), 'Scrolling map', key: key); + : super(const Icon(Icons.map), 'Scrolling map', key: key); @override Widget build(BuildContext context) { @@ -49,7 +49,7 @@ class ScrollingMapBody extends StatelessWidget { zoom: 11.0, ), gestureRecognizers: // - >{ + >{ Factory( () => EagerGestureRecognizer(), ), @@ -92,12 +92,12 @@ class ScrollingMapBody extends StatelessWidget { ), ), }, - gestureRecognizers: >{ - Factory( - () => ScaleGestureRecognizer(), - ), - }, + gestureRecognizers: + >{ + Factory( + () => ScaleGestureRecognizer(), + ), + }, ), ), ), diff --git a/packages/google_maps_flutter/example/lib/snapshot.dart b/packages/google_maps_flutter/example/lib/snapshot.dart index 53d9bc3e1..7158def17 100644 --- a/packages/google_maps_flutter/example/lib/snapshot.dart +++ b/packages/google_maps_flutter/example/lib/snapshot.dart @@ -18,11 +18,11 @@ const CameraPosition _kInitialPosition = CameraPosition( class SnapshotPage extends GoogleMapExampleAppPage { const SnapshotPage({Key? key}) - : super( - const Icon(Icons.camera_alt), - 'Take a snapshot of the map', - key: key, - ); + : super( + const Icon(Icons.camera_alt), + 'Take a snapshot of the map', + key: key, + ); @override Widget build(BuildContext context) { @@ -56,8 +56,8 @@ class _SnapshotBodyState extends State<_SnapshotBody> { TextButton( child: const Text('Take a snapshot'), onPressed: () async { - final Uint8List? imageBytes = - await _mapController?.takeSnapshot(); + final Uint8List? imageBytes = await _mapController + ?.takeSnapshot(); setState(() { _imageBytes = imageBytes; }); diff --git a/packages/google_maps_flutter/example/lib/tile_overlay.dart b/packages/google_maps_flutter/example/lib/tile_overlay.dart index 137e49e92..c3ce4e4e0 100644 --- a/packages/google_maps_flutter/example/lib/tile_overlay.dart +++ b/packages/google_maps_flutter/example/lib/tile_overlay.dart @@ -14,7 +14,7 @@ import 'page.dart'; class TileOverlayPage extends GoogleMapExampleAppPage { const TileOverlayPage({Key? key}) - : super(const Icon(Icons.map), 'Tile overlay', key: key); + : super(const Icon(Icons.map), 'Tile overlay', key: key); @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/example/pubspec.yaml index a78e6c283..afb2b744d 100644 --- a/packages/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/example/pubspec.yaml @@ -3,13 +3,13 @@ description: Demonstrates how to use the google_maps_flutter_tizen plugin. publish_to: "none" environment: - sdk: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + sdk: ^3.8.0 + flutter: ">=3.32.0" dependencies: flutter: sdk: flutter - google_maps_flutter: ^2.16.0 + google_maps_flutter: ^2.17.0 google_maps_flutter_platform_interface: ^2.15.0 google_maps_flutter_tizen: path: ../ diff --git a/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart b/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart index eb6c1ca62..c15a05427 100644 --- a/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart +++ b/packages/google_maps_flutter/lib/google_maps_flutter_tizen.dart @@ -7,7 +7,6 @@ library google_maps_flutter_tizen; import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; @@ -19,6 +18,7 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf import 'package:stream_transform/stream_transform.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'src/google_maps_js_bridge.dart'; import 'src/util.dart' as util; part 'src/circle.dart'; diff --git a/packages/google_maps_flutter/lib/src/circle.dart b/packages/google_maps_flutter/lib/src/circle.dart index 197f3e09a..4a7cea7f3 100644 --- a/packages/google_maps_flutter/lib/src/circle.dart +++ b/packages/google_maps_flutter/lib/src/circle.dart @@ -12,11 +12,11 @@ class CircleController { required util.GCircle circle, bool consumeTapEvents = false, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _circle = circle, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { - _addCircleEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _circle = circle, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { + _addCircleEvent(bridge); } util.GCircle? _circle; @@ -25,10 +25,13 @@ class CircleController { /// Circle component's tap event. ui.VoidCallback? tapEvent; - Future _addCircleEvent(WebViewController? controller) async { - final String command = - "$_circle.addListener('click', (event) => CircleClick.postMessage(JSON.stringify(${_circle?.id})));"; - await controller!.runJavaScript(command); + Future _addCircleEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_circle.toString()), + 'click', + 'CircleClick', + 'JSON.stringify(${_circle?.id})', + ); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/circles.dart b/packages/google_maps_flutter/lib/src/circles.dart index d2ec6b955..9f0a22a4a 100644 --- a/packages/google_maps_flutter/lib/src/circles.dart +++ b/packages/google_maps_flutter/lib/src/circles.dart @@ -8,10 +8,13 @@ part of '../google_maps_flutter_tizen.dart'; /// This class manages all the [CircleController]s associated to a [GoogleMapController]. class CirclesController extends GeometryController { /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - CirclesController({required StreamController> stream}) - : _streamController = stream, - _circleIdToController = {}, - _idToCircleId = {}; + CirclesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _circleIdToController = {}, + _idToCircleId = {}; // A cache of [CircleController]s indexed by their [CircleId]. final Map _circleIdToController; @@ -20,6 +23,8 @@ class CirclesController extends GeometryController { // The stream over which circles broadcast their events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Circle] objects to the cache. /// /// Wraps each [Circle] into its corresponding [CircleController]. @@ -35,14 +40,14 @@ class CirclesController extends GeometryController { final util.GCircleOptions populationOptions = _circleOptionsFromCircle( circle, ); - final util.GCircle gCircle = util.GCircle(populationOptions); + final util.GCircle gCircle = util.GCircle(_bridge, populationOptions); final CircleController controller = CircleController( circle: gCircle, consumeTapEvents: circle.consumeTapEvents, onTap: () { _onCircleTap(circle.circleId); }, - controller: util.webController, + bridge: _bridge, ); _idToCircleId[gCircle.id] = circle.circleId; _circleIdToController[circle.circleId] = controller; diff --git a/packages/google_maps_flutter/lib/src/convert.dart b/packages/google_maps_flutter/lib/src/convert.dart index 0e7d3b37a..1e6e13852 100644 --- a/packages/google_maps_flutter/lib/src/convert.dart +++ b/packages/google_maps_flutter/lib/src/convert.dart @@ -216,23 +216,24 @@ String _mapStyles(String? mapStyleJson) { if (mapStyleJson != null) { try { json - .decode( - mapStyleJson, - reviver: (Object? key, Object? value) { - if (value is Map && - _isJsonMapStyle(value as Map)) { - return MapTypeStyle() - ..elementType = value['elementType'] as String? - ..featureType = value['featureType'] as String? - ..stylers = (value['stylers']! as List) - .map((dynamic e) => e) - .toList(); - } - return value; - }, - ) - .cast() - .toList() as List; + .decode( + mapStyleJson, + reviver: (Object? key, Object? value) { + if (value is Map && + _isJsonMapStyle(value as Map)) { + return MapTypeStyle() + ..elementType = value['elementType'] as String? + ..featureType = value['featureType'] as String? + ..stylers = (value['stylers']! as List) + .map((dynamic e) => e) + .toList(); + } + return value; + }, + ) + .cast() + .toList() + as List; } catch (e) { throw MapStyleException('Invalid Map Style JSON: $e'); } @@ -306,9 +307,11 @@ util.GInfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { return null; } - // Add an outer wrapper to the contents of the infowindow + // Add an outer wrapper to the contents of the infowindow. The content is + // JSON-encoded by its consumers (GInfoWindowOptions.toString and + // GInfoWindow._setContent), so it must be raw, unquoted HTML here. final StringBuffer buffer = StringBuffer(); - buffer.write('\'
'); + buffer.write('
'); if (markerTitle.isNotEmpty) { buffer.write('

'); buffer.write(markerTitle); @@ -319,7 +322,7 @@ util.GInfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { buffer.write(markerSnippet); buffer.write('

'); } - buffer.write("
'"); + buffer.write(''); // Need to add Click Event to infoWindow's content return util.GInfoWindowOptions() @@ -461,7 +464,8 @@ util.GPolygonOptions _polygonOptionsFromPolygon(Polygon polygon) { bool _isPolygonClockwise(List path) { double direction = 0.0; for (int i = 0; i < path.length; i++) { - direction = direction + + direction = + direction + ((path[(i + 1) % path.length].latitude - path[i].latitude) * (path[(i + 1) % path.length].longitude + path[i].longitude)); } @@ -505,8 +509,9 @@ util.GGroundOverlayOptions? _groundOverlayOptionsFromGroundOverlay( return null; } return util.GGroundOverlayOptions() - ..url = "'$imageUrl'" - ..bounds = '{south:${bounds.southwest.latitude},' + ..url = imageUrl + ..bounds = + '{south:${bounds.southwest.latitude},' ' west:${bounds.southwest.longitude},' ' north:${bounds.northeast.latitude},' ' east:${bounds.northeast.longitude}}' diff --git a/packages/google_maps_flutter/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/lib/src/google_maps_controller.dart index 53e7c4ab8..957b1a61d 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_controller.dart @@ -7,9 +7,6 @@ part of '../google_maps_flutter_tizen.dart'; -/// The duration of MapLongPressEvent. -const int kGoogleMapsControllerLongPressDuration = 1000; - /// This class implements a Map Controller and its events class GoogleMapsController { /// Initializes the GoogleMapsController. @@ -24,28 +21,40 @@ class GoogleMapsController { Set clusterManagers = const {}, Set groundOverlays = const {}, Map mapOptions = const {}, - }) : _mapId = mapId, - _streamController = streamController, - _initialCameraPosition = initialCameraPosition, - _markers = markers, - _polygons = polygons, - _polylines = polylines, - _circles = circles, - _clusterManagers = clusterManagers, - _groundOverlays = groundOverlays, - _rawMapOptions = mapOptions { - _circlesController = CirclesController(stream: _streamController); - _polygonsController = PolygonsController(stream: _streamController); - _polylinesController = PolylinesController(stream: _streamController); + }) : _mapId = mapId, + _streamController = streamController, + _initialCameraPosition = initialCameraPosition, + _markers = markers, + _polygons = polygons, + _polylines = polylines, + _circles = circles, + _clusterManagers = clusterManagers, + _groundOverlays = groundOverlays, + _rawMapOptions = mapOptions { + _circlesController = CirclesController( + stream: _streamController, + bridge: _bridge, + ); + _polygonsController = PolygonsController( + stream: _streamController, + bridge: _bridge, + ); + _polylinesController = PolylinesController( + stream: _streamController, + bridge: _bridge, + ); _clusterManagersController = ClusterManagersController( stream: _streamController, + bridge: _bridge, ); _markersController = MarkersController( stream: _streamController, clusterManagersController: _clusterManagersController!, + bridge: _bridge, ); _groundOverlaysController = GroundOverlaysController( stream: _streamController, + bridge: _bridge, ); } @@ -61,14 +70,14 @@ class GoogleMapsController { final Set _circles; final Set _clusterManagers; final Set _groundOverlays; - final Completer _pageFinishedCompleter = Completer(); WebViewWidget? _webview; // The raw options passed by the user, before converting to maps. // Caching this allows us to re-create the map faithfully when needed. Map _rawMapOptions = {}; - /// Webview controller instance. - final WebViewController controller = WebViewController(); + /// The bridge mediating all interaction with the Google Maps JavaScript + /// API running inside the WebView. + final GoogleMapsJsBridge _bridge = GoogleMapsJsBridge(); /// The Flutter widget that will contain the rendered Map. Used for caching. WebViewWidget? get webview => _webview; @@ -76,9 +85,11 @@ class GoogleMapsController { /// Returns min-max zoom levels. Test only. @visibleForTesting Future getMinMaxZoomLevels() async { - final String value = await controller.runJavaScriptReturningResult( - 'JSON.stringify([map.minZoom, map.maxZoom])', - ) as String; + final String value = + await _bridge.runJavaScriptReturningResult( + 'JSON.stringify([map.minZoom, map.maxZoom])', + ) + as String; final dynamic bound = json.decode(value); double min = 0, max = 0; if (bound is List) { @@ -100,24 +111,26 @@ class GoogleMapsController { /// Returns if zoomGestures property is enabled. Test only. @visibleForTesting Future isZoomGesturesEnabled() async { - final String value = await controller - .runJavaScriptReturningResult('map.gestureHandling') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.gestureHandling') + as String; return value != 'none'; } /// Returns if zoomControls property is enabled. Test only. @visibleForTesting Future isZoomControlsEnabled() async { - final String value = await controller - .runJavaScriptReturningResult('map.zoomControl') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.zoomControl') as String; return value != 'false'; } /// Returns if scrollGestures property is enabled. Test only. @visibleForTesting Future isScrollGesturesEnabled() async { - final String value = await controller - .runJavaScriptReturningResult('map.gestureHandling') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.gestureHandling') + as String; return value != 'none'; } @@ -127,89 +140,58 @@ class GoogleMapsController { return _isTrafficLayerEnabled(_rawMapOptions); } + StreamSubscription? _bridgeEventsSubscription; + void _getWebview() { - // If the variable does not exist, we must find other alternatives. - String path = Platform.environment['AUL_ROOT_PATH'] ?? ''; - path += '/res/flutter_assets/assets/map.html'; - controller - ..setNavigationDelegate( - NavigationDelegate( - onPageFinished: (String url) { - _pageFinishedCompleter.complete(true); - }, - ), - ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..addJavaScriptChannel( - 'BoundChanged', - onMessageReceived: _onBoundsChanged, - ) - ..addJavaScriptChannel('Idle', onMessageReceived: _onIdle) - ..addJavaScriptChannel('Tilesloaded', onMessageReceived: _onTilesloaded) - ..addJavaScriptChannel('Click', onMessageReceived: _onClick) - ..addJavaScriptChannel('LongPress', onMessageReceived: _onLongPress) - ..addJavaScriptChannel('MarkerClick', onMessageReceived: _onMarkerClick) - ..addJavaScriptChannel('ClusterClick', onMessageReceived: _onClusterClick) - ..addJavaScriptChannel( - 'MarkerDragStart', - onMessageReceived: _onMarkerDragStart, - ) - ..addJavaScriptChannel('MarkerDrag', onMessageReceived: _onMarkerDrag) - ..addJavaScriptChannel( - 'MarkerDragEnd', - onMessageReceived: _onMarkerDragEnd, - ) - ..addJavaScriptChannel( - 'PolylineClick', - onMessageReceived: _onPolylineClick, - ) - ..addJavaScriptChannel('PolygonClick', onMessageReceived: _onPolygonClick) - ..addJavaScriptChannel('CircleClick', onMessageReceived: _onCircleClick) - ..addJavaScriptChannel( - 'GroundOverlayClick', - onMessageReceived: _onGroundOverlayClick, - ) - ..loadFile(path); - - _webview = WebViewWidget(controller: controller); + _bridgeEventsSubscription = _bridge.events.listen(_onJsEvent); + _webview = WebViewWidget(controller: _bridge.controller); } - Future _createMap() async { - final String options = _createOptions(); - final String command = ''' - map = new google.maps.Map(document.getElementById('map'), $options); - map.addListener('bounds_changed', (event) => { BoundChanged.postMessage(''); }); - map.addListener('idle', (event) => { Idle.postMessage(''); }); - map.addListener('click', (event) => { Click.postMessage(JSON.stringify(event)); }); - map.addListener('tilesloaded', (evnet) => { Tilesloaded.postMessage(''); }); - - let longPressTimeout; - map.addListener('mousedown', (e) => { - longPressTimeout = setTimeout(() => { - LongPress.postMessage(JSON.stringify(e)); - }, $kGoogleMapsControllerLongPressDuration); - }); - map.addListener('mouseup', () => { clearTimeout(longPressTimeout); }); - map.addListener('mouseout', () => { clearTimeout(longPressTimeout); }); - - const makeClusterEvent = function(clusterManagerId, event, cluster) { - var result = '{"id": "' + clusterManagerId +'"'; - result += ', "cluster": {"count":' + cluster.count - result += ', "position":' + JSON.stringify(cluster.position) - result += ', "bounds":' + JSON.stringify(cluster.bounds); - result += ', "markers": ['; - var i = 0; - for (; i < cluster.markers.length - 1; i++) { - result += cluster.markers[i].id; - result += ', '; - } - result += cluster.markers[i].id; - result += ']}}'; + // Events already queued when dispose() runs can still be delivered, and the + // subscription discards the returned Future, so guard and catch here rather + // than in each handler. + Future _onJsEvent(MapsJsEvent event) async { + if (_streamController.isClosed) { + return; + } + try { + switch (event.type) { + case MapsJsEventType.boundsChanged: + await _onBoundsChanged(); + case MapsJsEventType.idle: + _onIdle(); + case MapsJsEventType.tilesLoaded: + _onTilesloaded(); + case MapsJsEventType.click: + _onClick(event.message!); + case MapsJsEventType.longPress: + _onLongPress(event.message!); + case MapsJsEventType.markerClick: + _onMarkerClick(event.message!); + case MapsJsEventType.clusterClick: + _onClusterClick(event.message!); + case MapsJsEventType.markerDragStart: + _onMarkerDragStart(event.message!); + case MapsJsEventType.markerDrag: + _onMarkerDrag(event.message!); + case MapsJsEventType.markerDragEnd: + _onMarkerDragEnd(event.message!); + case MapsJsEventType.polylineClick: + _onPolylineClick(event.message!); + case MapsJsEventType.polygonClick: + _onPolygonClick(event.message!); + case MapsJsEventType.circleClick: + _onCircleClick(event.message!); + case MapsJsEventType.groundOverlayClick: + _onGroundOverlayClick(event.message!); + } + } catch (e) { + debugPrint('JavaScript Error: $e'); + } + } - return result; - } - '''; - await controller.runJavaScript(command); + Future _createMap() async { + await _bridge.createMap(_createOptions()); } String _createOptions() { @@ -237,7 +219,7 @@ class GoogleMapsController { // Keeps track if the map is moving or not. bool _mapIsMoving = false; - Future _onBoundsChanged(JavaScriptMessage message) async { + Future _onBoundsChanged() async { final LatLng center = await getCenter(); final num zoom = await getZoomLevel(); @@ -256,12 +238,12 @@ class GoogleMapsController { } } - void _onIdle(JavaScriptMessage message) { + void _onIdle() { _mapIsMoving = false; _streamController.add(CameraIdleEvent(_mapId)); } - void _onTilesloaded(JavaScriptMessage message) { + void _onTilesloaded() { try { if (_isFirst) { return; @@ -273,9 +255,9 @@ class GoogleMapsController { } } - void _onClick(JavaScriptMessage message) { + void _onClick(String message) { try { - final dynamic event = json.decode(message.message); + final dynamic event = json.decode(message); if (event is Map) { assert(event['latLng'] != null); final LatLng position = LatLng( @@ -289,9 +271,9 @@ class GoogleMapsController { } } - void _onLongPress(JavaScriptMessage message) { + void _onLongPress(String message) { try { - final dynamic event = json.decode(message.message); + final dynamic event = json.decode(message); if (event is Map) { assert(event['latLng'] != null); final LatLng position = LatLng( @@ -305,9 +287,9 @@ class GoogleMapsController { } } - void _onClusterClick(JavaScriptMessage message) { + void _onClusterClick(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); final String id = result['id'] as String; final ClusterManagerId? clusterManagerId = @@ -328,9 +310,9 @@ class GoogleMapsController { } } - void _onMarkerClick(JavaScriptMessage message) { + void _onMarkerClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_markersController != null && id is int) { final MarkerId? markerId = _markersController!._idToMarkerId[id]; final MarkerController? marker = @@ -344,9 +326,9 @@ class GoogleMapsController { } } - void _onMarkerDragStart(JavaScriptMessage message) { + void _onMarkerDragStart(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); if (result is Map) { assert(result['id'] != null && result['event'] != null); if (_markersController != null && result['id'] is int) { @@ -370,9 +352,9 @@ class GoogleMapsController { } } - void _onMarkerDrag(JavaScriptMessage message) { + void _onMarkerDrag(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); if (result is Map) { assert(result['id'] != null && result['event'] != null); if (_markersController != null && result['id'] is int) { @@ -396,9 +378,9 @@ class GoogleMapsController { } } - void _onMarkerDragEnd(JavaScriptMessage message) { + void _onMarkerDragEnd(String message) { try { - final dynamic result = json.decode(message.message); + final dynamic result = json.decode(message); if (result is Map) { assert(result['id'] != null && result['event'] != null); if (_markersController != null && result['id'] is int) { @@ -422,9 +404,9 @@ class GoogleMapsController { } } - void _onPolylineClick(JavaScriptMessage message) { + void _onPolylineClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_polylinesController != null && id is int) { final PolylineId? polylineId = _polylinesController!._idToPolylineId[id]; @@ -439,9 +421,9 @@ class GoogleMapsController { } } - void _onPolygonClick(JavaScriptMessage message) { + void _onPolygonClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_polygonsController != null && id is int) { final PolygonId? polygonId = _polygonsController!._idToPolygonId[id]; final PolygonController? polygon = @@ -455,10 +437,10 @@ class GoogleMapsController { } } - void _onCircleClick(JavaScriptMessage message) { + void _onCircleClick(String message) { try { - final dynamic id = json.decode(message.message); - if (_polygonsController != null && id is int) { + final dynamic id = json.decode(message); + if (_circlesController != null && id is int) { final CircleId? circleId = _circlesController!._idToCircleId[id]; final CircleController? circle = _circlesController!._circleIdToController[circleId]; @@ -471,9 +453,9 @@ class GoogleMapsController { } } - void _onGroundOverlayClick(JavaScriptMessage message) { + void _onGroundOverlayClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_groundOverlaysController != null && id is int) { final GroundOverlayId? groundOverlayId = _groundOverlaysController!._idToGroundOverlayId[id]; @@ -498,7 +480,12 @@ class GoogleMapsController { Future init() async { if (_webview == null && !_streamController.isClosed) { _getWebview(); - await _pageFinishedCompleter.future; + if (!await _bridge.load()) { + return; + } + if (_streamController.isClosed) { + return; + } await _createMap(); } await _attachGeometryControllers(); @@ -551,7 +538,6 @@ class GoogleMapsController { _clusterManagersController!.bindToMap(_mapId, _webview!); _groundOverlaysController!.bindToMap(_mapId, _webview!); - util.webController = controller; _controllersBoundToMap = true; } @@ -604,16 +590,17 @@ class GoogleMapsController { } Future _setOptions(String options) async { - await _callMethod(controller, 'setOptions', [options]); + await _callMethod('setOptions', [JsExpression(options)]); } - Future _setZoom(String options) async { - await _callMethod(controller, 'setZoom', [options]); + Future _setZoom(String zoom) async { + await _callMethod('setZoom', [JsExpression(zoom)]); } // Attaches/detaches a Traffic Layer on the `map` if `attach` is true/false. Future _setTrafficLayer(bool attach) async { - final String command = ''' + final String command = + ''' var trafficLayer; if ($attach == true && trafficLayer == null) { trafficLayer = new google.maps.TrafficLayer(); @@ -626,39 +613,32 @@ class GoogleMapsController { console.log('trafficLayer detached!!'); } '''; - await controller.runJavaScript(command); + await _bridge.runJavaScript(command); } Future _setMoveCamera(String options) async { - await _callMethod(controller, 'moveCamera', [options]); + await _callMethod('moveCamera', [JsExpression(options)]); } Future _setPanTo(String options) async { - await _callMethod(controller, 'panTo', [options]); + await _callMethod('panTo', [JsExpression(options)]); } - Future _setPanBy(String options) async { - await _callMethod(controller, 'panBy', [options]); + Future _setPanBy(num x, num y) async { + await _callMethod('panBy', [x, y]); } - Future _setFitBounds(String options) async { - await _callMethod(controller, 'fitBounds', [options]); + Future _setFitBounds(String boundsJs, Object? padding) async { + await _callMethod('fitBounds', [JsExpression(boundsJs), padding]); } - Future _callMethod( - WebViewController controller, - String method, - List args, - ) async { - return controller.runJavaScriptReturningResult( - 'JSON.stringify(map.$method.apply(map, $args))', - ); + Future _callMethod(String method, List args) async { + return _bridge.callMethodReturningJson(JsRef('map'), method, args); } - Future _getZoom(WebViewController controller) async { + Future _getZoom() async { try { - return (await _callMethod(controller, 'getZoom', []) as num) + - 0.0; + return (await _callMethod('getZoom', []) as num) + 0.0; } catch (e) { debugPrint('JavaScript Error: $e'); return 0.0; @@ -668,14 +648,14 @@ class GoogleMapsController { /// Returns the [LatLngBounds] of the current viewport. Future getVisibleRegion() async { return _convertToBounds( - await _callMethod(controller, 'getBounds', []) as String, + await _callMethod('getBounds', []) as String, ); } /// Returns the [LatLng] at the center of the map. Future getCenter() async { return _convertToLatLng( - await _callMethod(controller, 'getCenter', []) as String, + await _callMethod('getCenter', []) as String, ); } @@ -714,10 +694,11 @@ class GoogleMapsController { await _setPanTo('{lat:${json[1][0]}, lng: ${json[1][1]}}'); case 'newLatLngBounds': await _setFitBounds( - '{south:${json[1][0][0]}, west:${json[1][0][1]}, north:${json[1][1][0]}, east:${json[1][1][1]}}, ${json[2]}', + '{south:${json[1][0][0]}, west:${json[1][0][1]}, north:${json[1][1][0]}, east:${json[1][1][1]}}', + json[2], ); case 'scrollBy': - await _setPanBy('${json[1]}, ${json[2]}'); + await _setPanBy(json[1] as num, json[2] as num); case 'zoomBy': String? focusLatLng; double zoomDelta = 0.0; @@ -725,8 +706,9 @@ class GoogleMapsController { zoomDelta = (json[1] as num) + 0.0; } // Web only supports integer changes... - final int newZoomDelta = - zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); + final int newZoomDelta = zoomDelta < 0 + ? zoomDelta.floor() + : zoomDelta.ceil(); if (json.length == 3) { // With focus try { @@ -754,7 +736,8 @@ class GoogleMapsController { } Future _pixelToLatLng(double x, double y) async { - final String command = ''' + final String command = + ''' function getPixelToLatLng() { var projection = map.getProjection(); var ne = map.getBounds().getNorthEast(); @@ -768,11 +751,12 @@ class GoogleMapsController { JSON.stringify(getPixelToLatLng()); '''; - return await controller.runJavaScriptReturningResult(command) as String; + return await _bridge.runJavaScriptReturningResult(command) as String; } Future _latLngToPoint(LatLng latLng) async { - final String command = ''' + final String command = + ''' function getLatLngToPixel() { var ne = map.getBounds().getNorthEast(); var sw = map.getBounds().getSouthWest(); @@ -788,12 +772,12 @@ class GoogleMapsController { JSON.stringify(getLatLngToPixel()); '''; - return await controller.runJavaScriptReturningResult(command) as String; + return await _bridge.runJavaScriptReturningResult(command) as String; } /// Returns the zoom level of the current viewport. Future getZoomLevel() async { - return _getZoom(controller); + return _getZoom(); } // Geometry manipulation @@ -901,6 +885,8 @@ class GoogleMapsController { /// You won't be able to call many of the methods on this controller after /// calling `dispose`! void dispose() { + unawaited(_bridgeEventsSubscription?.cancel()); + _bridge.dispose(); _webview = null; _circlesController = null; _polygonsController = null; diff --git a/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart new file mode 100644 index 000000000..5780ab702 --- /dev/null +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -0,0 +1,338 @@ +// Copyright 2026 Samsung Electronics Co., Ltd. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:webview_flutter/webview_flutter.dart'; + +/// The duration (in milliseconds) of mouse-down before it is treated as a +/// long press by the JS side of the map. +const int kGoogleMapsControllerLongPressDuration = 1000; + +/// A handle to a JavaScript object living inside the Google Maps WebView. +/// +/// Wraps the JS-side variable name so it can be interpolated into further +/// JavaScript snippets (via [toString]) without callers hand-building +/// variable names. +class JsRef { + /// Creates a handle to the JS-side variable called [name]. + JsRef(this.name); + + /// The JS-side variable name this handle refers to. + final String name; + + @override + String toString() => name; +} + +/// A piece of raw JavaScript code to be evaluated literally rather than +/// encoded as a string literal. +class JsExpression { + /// Creates a [JsExpression] wrapping the raw [code]. + const JsExpression(this.code); + + /// The raw JavaScript code. + final String code; + + @override + String toString() => code; +} + +/// Identifies the kind of a [MapsJsEvent] dispatched from the Google Maps +/// JavaScript runtime. +enum MapsJsEventType { + /// The map's `bounds_changed` listener fired. + boundsChanged, + + /// The map's `idle` listener fired. + idle, + + /// The map's `tilesloaded` listener fired. + tilesLoaded, + + /// The map was clicked. + click, + + /// The map was long-pressed. + longPress, + + /// A marker was clicked. + markerClick, + + /// A marker cluster was clicked. + clusterClick, + + /// A marker drag started. + markerDragStart, + + /// A marker is being dragged. + markerDrag, + + /// A marker drag ended. + markerDragEnd, + + /// A polyline was clicked. + polylineClick, + + /// A polygon was clicked. + polygonClick, + + /// A circle was clicked. + circleClick, + + /// A ground overlay was clicked. + groundOverlayClick, +} + +/// An event dispatched from the Google Maps JavaScript runtime back into +/// Dart through a [GoogleMapsJsBridge]. +/// +/// [message] carries the raw (JSON-encoded) payload posted from the JS side, +/// or `null` for a [type] that carries no payload. +typedef MapsJsEvent = ({MapsJsEventType type, String? message}); + +/// Mediates all interaction between Dart and the Google Maps JavaScript API +/// running inside a WebView. +/// +/// This is the single seam through which JS commands are built and JS→Dart +/// events are dispatched, replacing ad hoc `runJavaScript` calls and +/// hand-built JS strings scattered across the plugin. +class GoogleMapsJsBridge { + /// Creates a bridge over a fresh [WebViewController]. + GoogleMapsJsBridge() : controller = WebViewController(); + + /// The JS-side channel name for each event type, registered in [load]. + static const Map _channelEventTypes = + { + 'BoundChanged': MapsJsEventType.boundsChanged, + 'Idle': MapsJsEventType.idle, + 'Tilesloaded': MapsJsEventType.tilesLoaded, + 'Click': MapsJsEventType.click, + 'LongPress': MapsJsEventType.longPress, + 'MarkerClick': MapsJsEventType.markerClick, + 'ClusterClick': MapsJsEventType.clusterClick, + 'MarkerDragStart': MapsJsEventType.markerDragStart, + 'MarkerDrag': MapsJsEventType.markerDrag, + 'MarkerDragEnd': MapsJsEventType.markerDragEnd, + 'PolylineClick': MapsJsEventType.polylineClick, + 'PolygonClick': MapsJsEventType.polygonClick, + 'CircleClick': MapsJsEventType.circleClick, + 'GroundOverlayClick': MapsJsEventType.groundOverlayClick, + }; + + /// Event types whose JS side posts an empty payload, so [MapsJsEvent] is + /// created with a `null` [MapsJsEvent.message] instead of `''`. + static const Set _payloadlessEventTypes = { + MapsJsEventType.boundsChanged, + MapsJsEventType.idle, + MapsJsEventType.tilesLoaded, + }; + + /// The underlying WebView controller. Exposed so callers can build the + /// [WebViewWidget] that hosts this bridge's JS runtime. + final WebViewController controller; + + final StreamController _events = + StreamController.broadcast(); + final Completer _pageFinished = Completer(); + + /// Broadcasts events received from the JS side. + Stream get events => _events.stream; + + /// Adds [event] to [_events], unless this bridge has already been + /// disposed. + /// + /// JS-side timers and in-flight `postMessage` calls can still invoke the + /// channel callbacks below after [dispose] closes [_events], so emission + /// must be guarded rather than left to throw on a closed controller. + void _emit(MapsJsEvent event) { + if (!_events.isClosed) { + _events.add(event); + } + } + + /// Loads the map HTML shell and wires up the JS→Dart event channels. + /// + /// Completes with `true` once the page has finished loading, or with + /// `false` if this bridge was disposed before that happened. + Future load() { + String path = Platform.environment['AUL_ROOT_PATH'] ?? ''; + path += '/res/flutter_assets/assets/map.html'; + controller + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (String url) { + if (!_pageFinished.isCompleted) { + _pageFinished.complete(true); + } + }, + ), + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted); + + for (final MapEntry entry + in _channelEventTypes.entries) { + final MapsJsEventType type = entry.value; + controller.addJavaScriptChannel( + entry.key, + onMessageReceived: (JavaScriptMessage message) { + _emit(( + type: type, + message: _payloadlessEventTypes.contains(type) + ? null + : message.message, + )); + }, + ); + } + + controller.loadFile(path); + + return _pageFinished.future; + } + + /// Creates the top-level `map` JS variable using [optionsJs] (a JS object + /// literal), plus its built-in map-level listeners. + Future createMap(String optionsJs) async { + final String command = + ''' + map = new google.maps.Map(document.getElementById('map'), $optionsJs); + map.addListener('bounds_changed', (event) => { BoundChanged.postMessage(''); }); + map.addListener('idle', (event) => { Idle.postMessage(''); }); + map.addListener('click', (event) => { Click.postMessage(JSON.stringify(event)); }); + map.addListener('tilesloaded', (evnet) => { Tilesloaded.postMessage(''); }); + + let longPressTimeout; + map.addListener('mousedown', (e) => { + longPressTimeout = setTimeout(() => { + LongPress.postMessage(JSON.stringify(e)); + }, $kGoogleMapsControllerLongPressDuration); + }); + map.addListener('mouseup', () => { clearTimeout(longPressTimeout); }); + map.addListener('mouseout', () => { clearTimeout(longPressTimeout); }); + + const makeClusterEvent = function(clusterManagerId, event, cluster) { + var result = '{"id": "' + clusterManagerId +'"'; + result += ', "cluster": {"count":' + cluster.count + result += ', "position":' + JSON.stringify(cluster.position) + result += ', "bounds":' + JSON.stringify(cluster.bounds); + result += ', "markers": ['; + var i = 0; + for (; i < cluster.markers.length - 1; i++) { + result += cluster.markers[i].id; + result += ', '; + } + result += cluster.markers[i].id; + result += ']}}'; + + return result; + } + '''; + await controller.runJavaScript(command); + } + + /// Creates a JS object via `new `, assigns it to + /// the JS-side variable [varName], and returns a [JsRef] handle to it. + Future createObject( + String varName, + String constructorExpression, + ) async { + await controller.runJavaScript('var $varName = $constructorExpression;'); + return JsRef(varName); + } + + /// Serializes [arg] for interpolation into a JavaScript snippet. + /// + /// [JsRef]s and [JsExpression]s are emitted as raw JS code via their + /// [toString], so they refer to JS-side variables/expressions. Plain + /// [String]s are JSON-encoded so they are safely quoted and escaped as JS + /// string literals rather than being mistaken for raw code. + String _serializeArg(Object? arg) { + if (arg is JsRef || arg is JsExpression) { + return arg.toString(); + } + if (arg is String) { + return jsonEncode(arg); + } + return arg.toString(); + } + + /// Assigns `ref[property] = value` on the JS side. + Future setProperty(JsRef ref, String property, Object? value) async { + await controller.runJavaScript( + "JSON.stringify($ref['$property'] = ${_serializeArg(value)})", + ); + } + + /// Calls `ref.method(...args)` on the JS side, discarding the result. + Future callMethod(JsRef ref, String method, List args) async { + final String serializedArgs = '[${args.map(_serializeArg).join(', ')}]'; + await controller.runJavaScript( + 'JSON.stringify($ref.$method.apply($ref, $serializedArgs))', + ); + } + + /// Calls `ref.method(...args)` on the JS side and returns the JSON-encoded + /// result. + Future callMethodReturningJson( + JsRef ref, + String method, + List args, + ) async { + final String serializedArgs = '[${args.map(_serializeArg).join(', ')}]'; + return controller.runJavaScriptReturningResult( + 'JSON.stringify($ref.$method.apply($ref, $serializedArgs))', + ); + } + + /// Calls `ref.method(...args)` on the JS side and returns the result. + Future callMethodReturning( + JsRef ref, + String method, + List args, + ) async { + final String serializedArgs = '[${args.map(_serializeArg).join(', ')}]'; + return controller.runJavaScriptReturningResult( + '$ref.$method.apply($ref, $serializedArgs)', + ); + } + + /// Registers `ref.addListener(eventName, ...)` on the JS side, so that + /// [payloadJs] (a JS expression, evaluated with `event` bound to the + /// listener's callback argument) is posted to [channel] whenever it fires. + Future addListener( + JsRef ref, + String eventName, + String channel, + String payloadJs, + ) async { + await controller.runJavaScript( + "$ref.addListener('$eventName', (event) => $channel.postMessage($payloadJs));", + ); + } + + /// Escape hatch for JS not yet expressed in terms of the methods above. + Future runJavaScript(String script) async { + await controller.runJavaScript(script); + } + + /// Escape hatch for JS not yet expressed in terms of the methods above, + /// returning the raw result. + Future runJavaScriptReturningResult(String script) async { + return controller.runJavaScriptReturningResult(script); + } + + /// Releases the resources held by this bridge. + void dispose() { + // Unblocks a `load()` awaiting a page-finished callback that will never + // arrive, which would otherwise hang forever and leak the controller graph. + if (!_pageFinished.isCompleted) { + _pageFinished.complete(false); + } + _events.close(); + } +} diff --git a/packages/google_maps_flutter/lib/src/ground_overlay.dart b/packages/google_maps_flutter/lib/src/ground_overlay.dart index 94d4a7f59..0414e909b 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlay.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlay.dart @@ -13,10 +13,10 @@ class GroundOverlayController { GroundOverlayController({ required util.GGroundOverlay groundOverlay, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _groundOverlay = groundOverlay, - tapEvent = onTap { - _addGroundOverlayEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _groundOverlay = groundOverlay, + tapEvent = onTap { + _addGroundOverlayEvent(bridge); } util.GGroundOverlay? _groundOverlay; @@ -24,10 +24,13 @@ class GroundOverlayController { /// Ground overlay component's tap event. ui.VoidCallback? tapEvent; - Future _addGroundOverlayEvent(WebViewController? controller) async { - final String command = - "$_groundOverlay.addListener('click', (event) => GroundOverlayClick.postMessage(JSON.stringify(${_groundOverlay?.id})));"; - await controller!.runJavaScript(command); + Future _addGroundOverlayEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_groundOverlay.toString()), + 'click', + 'GroundOverlayClick', + 'JSON.stringify(${_groundOverlay?.id})', + ); } /// Updates the options of the wrapped [GGroundOverlay] object. diff --git a/packages/google_maps_flutter/lib/src/ground_overlays.dart b/packages/google_maps_flutter/lib/src/ground_overlays.dart index 470a50021..2d89d16e7 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlays.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlays.dart @@ -12,19 +12,23 @@ class GroundOverlaysController extends GeometryController { /// [GoogleMapController], and is shared with other controllers. GroundOverlaysController({ required StreamController> stream, - }) : _streamController = stream, - _groundOverlayIdToController = - {}, - _idToGroundOverlayId = {}; + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _groundOverlayIdToController = + {}, + _idToGroundOverlayId = {}; // A cache of [GroundOverlayController]s indexed by their [GroundOverlayId]. final Map - _groundOverlayIdToController; + _groundOverlayIdToController; final Map _idToGroundOverlayId; // The stream over which ground overlays broadcast events. final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [GroundOverlay] objects to the cache. /// /// Wraps each [GroundOverlay] into its corresponding @@ -45,6 +49,7 @@ class GroundOverlaysController extends GeometryController { } final util.GGroundOverlay gGroundOverlay = util.GGroundOverlay( + _bridge, populationOptions, ); final GroundOverlayController controller = GroundOverlayController( @@ -52,7 +57,7 @@ class GroundOverlaysController extends GeometryController { onTap: () { _onGroundOverlayTap(groundOverlay.groundOverlayId); }, - controller: util.webController, + bridge: _bridge, ); _idToGroundOverlayId[gGroundOverlay.id] = groundOverlay.groundOverlayId; _groundOverlayIdToController[groundOverlay.groundOverlayId] = controller; diff --git a/packages/google_maps_flutter/lib/src/marker.dart b/packages/google_maps_flutter/lib/src/marker.dart index 04c3e3ce8..6b258c1bc 100644 --- a/packages/google_maps_flutter/lib/src/marker.dart +++ b/packages/google_maps_flutter/lib/src/marker.dart @@ -17,17 +17,17 @@ class MarkerController { LatLngCallback? onDragEnd, ui.VoidCallback? onTap, ClusterManagerId? clusterManagerId, - WebViewController? controller, - }) : _marker = marker, - _infoWindow = infoWindow, - _consumeTapEvents = consumeTapEvents, - _clusterManagerId = clusterManagerId, - tapEvent = onTap, - dragStartEvent = onDragStart, - dragEvent = onDrag, - dragEndEvent = onDragEnd { - if (controller != null) { - _addMarkerEvent(controller); + GoogleMapsJsBridge? bridge, + }) : _marker = marker, + _infoWindow = infoWindow, + _consumeTapEvents = consumeTapEvents, + _clusterManagerId = clusterManagerId, + tapEvent = onTap, + dragStartEvent = onDragStart, + dragEvent = onDrag, + dragEndEvent = onDragEnd { + if (bridge != null) { + _addMarkerEvent(bridge); } } @@ -49,13 +49,19 @@ class MarkerController { /// Marker component's drag end event. LatLngCallback? dragEndEvent; - Future _addMarkerEvent(WebViewController? controller) async { - final String command = ''' - $marker.addListener("click", (event) => MarkerClick.postMessage(JSON.stringify(${marker?.id}))); - $marker.addListener("dragstart", (event) => MarkerDragStart.postMessage(JSON.stringify({id:${marker?.id}, event:event}))); - $marker.addListener("drag", (event) => MarkerDrag.postMessage(JSON.stringify({id:${marker?.id}, event:event}))); - $marker.addListener("dragend", (event) => MarkerDragEnd.postMessage(JSON.stringify({id:${marker?.id}, event:event})));'''; - await controller!.runJavaScript(command); + Future _addMarkerEvent(GoogleMapsJsBridge bridge) async { + final JsRef ref = JsRef(_marker!.toString()); + final int id = _marker!.id; + final String dragPayload = 'JSON.stringify({id:$id, event:event})'; + await bridge.addListener( + ref, + 'click', + 'MarkerClick', + 'JSON.stringify($id)', + ); + await bridge.addListener(ref, 'dragstart', 'MarkerDragStart', dragPayload); + await bridge.addListener(ref, 'drag', 'MarkerDrag', dragPayload); + await bridge.addListener(ref, 'dragend', 'MarkerDragEnd', dragPayload); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/marker_clustering.dart b/packages/google_maps_flutter/lib/src/marker_clustering.dart index 9d7cf674c..81b1d96bb 100644 --- a/packages/google_maps_flutter/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/lib/src/marker_clustering.dart @@ -17,17 +17,21 @@ class ClusterManagersController extends GeometryController { /// emitting map events. ClusterManagersController({ required StreamController> stream, - }) : _streamController = stream, - _idToClusterManagerId = {}, - _clusterManagerIdToMarkerClusterer = - {}; + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _idToClusterManagerId = {}, + _clusterManagerIdToMarkerClusterer = + {}; // The stream over which cluster managers broadcast their events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + // A cache of [MarkerClusterer]s indexed by their [ClusterManagerId]. final Map - _clusterManagerIdToMarkerClusterer; + _clusterManagerIdToMarkerClusterer; final Map _idToClusterManagerId; /// A cache of [ClusterManagerId]s indexed by [GMarkerClusterer.id]. @@ -48,6 +52,7 @@ class ClusterManagersController extends GeometryController { ); final util.GMarkerClusterer markerClusterer = util.GMarkerClusterer( + _bridge, options, ); @@ -55,7 +60,13 @@ class ClusterManagersController extends GeometryController { markerClusterer; _idToClusterManagerId[clusterManager.clusterManagerId.value] = clusterManager.clusterManagerId; - markerClusterer.onAdd(); + // The platform interface requires this to stay synchronous, so the JS + // call is fire-and-forget; only its errors need surfacing. + unawaited( + markerClusterer.onAdd().catchError( + (Object e) => debugPrint('JavaScript Error: $e'), + ), + ); } /// Removes a set of [ClusterManagerId]s from the cache. @@ -67,8 +78,16 @@ class ClusterManagersController extends GeometryController { final util.GMarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { - markerClusterer.clearMarkers(true); - markerClusterer.onRemove(); + unawaited( + markerClusterer + .clearMarkers(true) + .catchError((Object e) => debugPrint('JavaScript Error: $e')), + ); + unawaited( + markerClusterer.onRemove().catchError( + (Object e) => debugPrint('JavaScript Error: $e'), + ), + ); } _clusterManagerIdToMarkerClusterer.remove(clusterManagerId); } @@ -79,8 +98,16 @@ class ClusterManagersController extends GeometryController { final util.GMarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { - markerClusterer.addMarker(marker, true); - markerClusterer.render(); + unawaited( + markerClusterer + .addMarker(marker, true) + .catchError((Object e) => debugPrint('JavaScript Error: $e')), + ); + unawaited( + markerClusterer.render().catchError( + (Object e) => debugPrint('JavaScript Error: $e'), + ), + ); } } @@ -91,19 +118,28 @@ class ClusterManagersController extends GeometryController { final util.GMarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { - markerClusterer.removeMarker(marker, true); - markerClusterer.render(); + unawaited( + markerClusterer.removeMarker(marker, true).catchError((Object e) { + debugPrint('JavaScript Error: $e'); + return false; + }), + ); + unawaited( + markerClusterer.render().catchError( + (Object e) => debugPrint('JavaScript Error: $e'), + ), + ); } } } /// Returns list of clusters in [MarkerClusterer] with given /// [ClusterManagerId]. - List getClusters(ClusterManagerId clusterManagerId) { + Future> getClusters(ClusterManagerId clusterManagerId) async { final util.GMarkerClusterer? markerClusterer = _clusterManagerIdToMarkerClusterer[clusterManagerId]; if (markerClusterer != null) { - return markerClusterer.clusters + return (await markerClusterer.clusters) .map( (Map cluster) => _convertCluster(clusterManagerId, cluster), diff --git a/packages/google_maps_flutter/lib/src/markers.dart b/packages/google_maps_flutter/lib/src/markers.dart index 41b5e7450..e09dbe2fd 100644 --- a/packages/google_maps_flutter/lib/src/markers.dart +++ b/packages/google_maps_flutter/lib/src/markers.dart @@ -11,10 +11,12 @@ class MarkersController extends GeometryController { MarkersController({ required StreamController> stream, required ClusterManagersController clusterManagersController, - }) : _streamController = stream, - _clusterManagersController = clusterManagersController, - _idToMarkerId = {}, - _markerIdToController = {}; + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _clusterManagersController = clusterManagersController, + _bridge = bridge, + _idToMarkerId = {}, + _markerIdToController = {}; // A cache of [MarkerController]s indexed by their [MarkerId]. final Map _markerIdToController; @@ -25,6 +27,8 @@ class MarkersController extends GeometryController { final ClusterManagersController _clusterManagersController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Marker] objects to the cache. /// /// Wraps each [Marker] into its corresponding [MarkerController]. @@ -42,14 +46,14 @@ class MarkersController extends GeometryController { util.GInfoWindow? infoWindow; if (infoWindowOptions != null) { - infoWindow = util.GInfoWindow(infoWindowOptions); + infoWindow = util.GInfoWindow(_bridge, infoWindowOptions); } final util.GMarkerOptions populationOptions = _markerOptionsFromMarker( marker, _markerIdToController[marker.markerId]?.marker, ); - final util.GMarker gMarker = util.GMarker(populationOptions); + final util.GMarker gMarker = util.GMarker(_bridge, populationOptions); if (marker.clusterManagerId != null) { _clusterManagersController.addItem(marker.clusterManagerId!, gMarker); @@ -73,7 +77,7 @@ class MarkersController extends GeometryController { onDragEnd: (LatLng latLng) { _onMarkerDragEnd(marker.markerId, latLng); }, - controller: util.webController, + bridge: _bridge, ); _idToMarkerId[gMarker.id] = marker.markerId; _markerIdToController[marker.markerId] = markerController; @@ -174,10 +178,11 @@ class MarkersController extends GeometryController { void _hideAllMarkerInfoWindow() { _markerIdToController.values .where( - (MarkerController? controller) => controller?.infoWindowShown ?? false, - ) + (MarkerController? controller) => + controller?.infoWindowShown ?? false, + ) .forEach((MarkerController controller) { - controller.hideInfoWindow(); - }); + controller.hideInfoWindow(); + }); } } diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart index e15d70e43..82b43a0e7 100644 --- a/packages/google_maps_flutter/lib/src/polygon.dart +++ b/packages/google_maps_flutter/lib/src/polygon.dart @@ -12,11 +12,11 @@ class PolygonController { required util.GPolygon polygon, bool consumeTapEvents = false, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _polygon = polygon, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { - _addPolygonEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _polygon = polygon, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { + _addPolygonEvent(bridge); } util.GPolygon? _polygon; @@ -25,10 +25,13 @@ class PolygonController { /// Polygon component's tap event. ui.VoidCallback? tapEvent; - Future _addPolygonEvent(WebViewController? controller) async { - final String command = - "$_polygon.addListener('click', (event) => PolygonClick.postMessage(JSON.stringify(${_polygon?.id})));"; - await controller!.runJavaScript(command); + Future _addPolygonEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_polygon.toString()), + 'click', + 'PolygonClick', + 'JSON.stringify(${_polygon?.id})', + ); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/polygons.dart b/packages/google_maps_flutter/lib/src/polygons.dart index baf7aba5f..5690f53c4 100644 --- a/packages/google_maps_flutter/lib/src/polygons.dart +++ b/packages/google_maps_flutter/lib/src/polygons.dart @@ -8,10 +8,13 @@ part of '../google_maps_flutter_tizen.dart'; /// This class manages a set of [PolygonController]s associated to a [GoogleMapController]. class PolygonsController extends GeometryController { /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - PolygonsController({required StreamController> stream}) - : _streamController = stream, - _polygonIdToController = {}, - _idToPolygonId = {}; + PolygonsController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _polygonIdToController = {}, + _idToPolygonId = {}; // A cache of [PolygonController]s indexed by their [PolygonId]. final Map _polygonIdToController; @@ -20,6 +23,8 @@ class PolygonsController extends GeometryController { // The stream over which polygons broadcast events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Polygon] objects to the cache. /// /// Wraps each Polygon into its corresponding [PolygonController]. @@ -35,14 +40,14 @@ class PolygonsController extends GeometryController { final util.GPolygonOptions populationOptions = _polygonOptionsFromPolygon( polygon, ); - final util.GPolygon gPolygon = util.GPolygon(populationOptions); + final util.GPolygon gPolygon = util.GPolygon(_bridge, populationOptions); final PolygonController controller = PolygonController( polygon: gPolygon, consumeTapEvents: polygon.consumeTapEvents, onTap: () { _onPolygonTap(polygon.polygonId); }, - controller: util.webController, + bridge: _bridge, ); _idToPolygonId[gPolygon.id] = polygon.polygonId; _polygonIdToController[polygon.polygonId] = controller; diff --git a/packages/google_maps_flutter/lib/src/polyline.dart b/packages/google_maps_flutter/lib/src/polyline.dart index b6324761e..489a99fb1 100644 --- a/packages/google_maps_flutter/lib/src/polyline.dart +++ b/packages/google_maps_flutter/lib/src/polyline.dart @@ -12,11 +12,11 @@ class PolylineController { required util.GPolyline polyline, bool consumeTapEvents = false, ui.VoidCallback? onTap, - WebViewController? controller, - }) : _polyline = polyline, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { - _addPolylineEvent(controller); + required GoogleMapsJsBridge bridge, + }) : _polyline = polyline, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { + _addPolylineEvent(bridge); } util.GPolyline? _polyline; @@ -25,10 +25,13 @@ class PolylineController { /// Polyline component's tap event. ui.VoidCallback? tapEvent; - Future _addPolylineEvent(WebViewController? controller) async { - final String command = - "$_polyline.addListener('click', (event) => PolylineClick.postMessage(JSON.stringify(${_polyline?.id})));"; - await controller!.runJavaScript(command); + Future _addPolylineEvent(GoogleMapsJsBridge bridge) async { + await bridge.addListener( + JsRef(_polyline.toString()), + 'click', + 'PolylineClick', + 'JSON.stringify(${_polyline?.id})', + ); } /// Returns `true` if this Controller will use its own `onTap` handler to consume events. diff --git a/packages/google_maps_flutter/lib/src/polylines.dart b/packages/google_maps_flutter/lib/src/polylines.dart index 55d55ea0c..6d9cfe3b0 100644 --- a/packages/google_maps_flutter/lib/src/polylines.dart +++ b/packages/google_maps_flutter/lib/src/polylines.dart @@ -8,10 +8,13 @@ part of '../google_maps_flutter_tizen.dart'; /// This class manages a set of [PolylinesController]s associated to a [GoogleMapController]. class PolylinesController extends GeometryController { /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. - PolylinesController({required StreamController> stream}) - : _streamController = stream, - _polylineIdToController = {}, - _idToPolylineId = {}; + PolylinesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, + _polylineIdToController = {}, + _idToPolylineId = {}; // A cache of [PolylineController]s indexed by their [PolylineId]. final Map _polylineIdToController; @@ -20,6 +23,8 @@ class PolylinesController extends GeometryController { // The stream over which polylines broadcast their events final StreamController> _streamController; + final GoogleMapsJsBridge _bridge; + /// Adds a set of [Polyline] objects to the cache. /// /// Wraps each line into its corresponding [PolylineController]. @@ -35,14 +40,14 @@ class PolylinesController extends GeometryController { final util.GPolylineOptions polylineOptions = _polylineOptionsFromPolyline( polyline, ); - final util.GPolyline gPolyline = util.GPolyline(polylineOptions); + final util.GPolyline gPolyline = util.GPolyline(_bridge, polylineOptions); final PolylineController controller = PolylineController( polyline: gPolyline, consumeTapEvents: polyline.consumeTapEvents, onTap: () { _onPolylineTap(polyline.polylineId); }, - controller: util.webController, + bridge: _bridge, ); _idToPolylineId[gPolyline.id] = polyline.polylineId; _polylineIdToController[polyline.polylineId] = controller; diff --git a/packages/google_maps_flutter/lib/src/util.dart b/packages/google_maps_flutter/lib/src/util.dart index 4e8b7fdc7..c4177a847 100644 --- a/packages/google_maps_flutter/lib/src/util.dart +++ b/packages/google_maps_flutter/lib/src/util.dart @@ -6,8 +6,11 @@ // ignore_for_file: avoid_setters_without_getters import 'dart:async'; +import 'dart:convert'; + import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:webview_flutter/webview_flutter.dart'; + +import 'google_maps_js_bridge.dart'; /// Default LatLng. const LatLng nullLatLng = LatLng(0, 0); @@ -54,7 +57,7 @@ class GMarkerOptions { String toString() { return '{anchorPoint:$anchorPoint, draggable:$draggable, icon:$icon, map: map, ' ' opacity:$opacity, position:new google.maps.LatLng(${position?.latitude}, ${position?.longitude}),' - ' title:"$title", visible:$visible, zIndex:$zIndex}'; + ' title:${jsonEncode(title)}, visible:$visible, zIndex:$zIndex}'; } } @@ -74,7 +77,7 @@ class GIcon { @override String toString() { - return '{url: "$url", scaledSize:$scaledSize, size: $size}'; + return '{url:${jsonEncode(url)}, scaledSize:$scaledSize, size: $size}'; } } @@ -141,21 +144,24 @@ class GInfoWindowOptions { final String pos = position != null ? '{lat:${position?.latitude}, lng:${position?.longitude}}' : 'null'; - return '{content:$content, pixelOffset:null , position:$pos, zIndex:$zIndex}'; + final String contentJs = content != null ? jsonEncode(content) : 'null'; + return '{content:$contentJs, pixelOffset:null , position:$pos, zIndex:$zIndex}'; } } /// This class represents GMarker's InfoWindow. class GInfoWindow { /// GInfoWindow Constructor. - GInfoWindow(GInfoWindowOptions? opts) : _id = _gid++ { + GInfoWindow(GoogleMapsJsBridge bridge, GInfoWindowOptions? opts) + : _bridge = bridge, + _id = _gid++ { _createInfoWindow(opts); } + final GoogleMapsJsBridge _bridge; + Future _createInfoWindow(GInfoWindowOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.InfoWindow($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.InfoWindow($opts)'); } final int _id; @@ -167,7 +173,7 @@ class GInfoWindow { } Future _callCloseInfoWindow() async { - await webController!.runJavaScript('${toString()}.close();'); + await _bridge.callMethod(JsRef(toString()), 'close', []); } /// Opens InfoWindow on the given map. @@ -176,9 +182,9 @@ class GInfoWindow { } Future _callOpenInfoWindow(GMarker? anchor) async { - await webController!.runJavaScript( - '${toString()}.open({anchor: $anchor, map});', - ); + await _bridge.callMethod(JsRef(toString()), 'open', [ + JsExpression('{anchor: $anchor, map}'), + ]); } @override @@ -193,27 +199,38 @@ class GInfoWindow { set pixelOffset(GSize? size) => _setPixelOffset(size); Future _setContent(Object? /*String?|Node?*/ content) async { - await callMethod(this, 'setContent', [content]); + await _bridge.callMethod(JsRef(toString()), 'setContent', [ + content, + ]); } Future _setPixelOffset(GSize? size) async { - await setProperty(this, 'pixelOffset', size?.toValue()); + await _bridge.setProperty( + JsRef(toString()), + 'pixelOffset', + size != null ? JsExpression(size.toValue()) : null, + ); } } /// This class represents a geographical location on the map as a Marker. class GMarker { /// GMarker Constructor. - GMarker([GMarkerOptions? opts]) - : id = _gid++, - _options = opts { + GMarker(GoogleMapsJsBridge bridge, [GMarkerOptions? opts]) + : _bridge = bridge, + id = _gid++, + _options = opts { _createMarker(opts); } + final GoogleMapsJsBridge _bridge; + Future _createMarker(GMarkerOptions? opts) async { - final String command = - 'var ${toString()} = new google.maps.Marker($opts); ${toString()}.id = $id;'; - await webController!.runJavaScript(command); + final JsRef ref = await _bridge.createObject( + toString(), + 'new google.maps.Marker($opts)', + ); + await _bridge.setProperty(ref, 'id', id); } /// GMarker id. @@ -274,41 +291,48 @@ class GMarker { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GMarkerOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod(JsRef(toString()), 'setOptions', [ + options, + ]); } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setDraggable(bool? visible) async { - await callMethod(this, 'setDraggable', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setDraggable', [ + visible, + ]); } Future _setIcon(Object? icon) async { - await callMethod(this, 'setIcon', [icon]); + await _bridge.callMethod(JsRef(toString()), 'setIcon', [icon]); } Future _setOpacity(num? opacity) async { - await callMethod(this, 'setOpacity', [opacity]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [opacity]); } Future _setPosition(LatLng? position) async { - await callMethod(this, 'setPosition', [ - 'new google.maps.LatLng(${position!.latitude},${position.longitude})', + await _bridge.callMethod(JsRef(toString()), 'setPosition', [ + JsExpression( + 'new google.maps.LatLng(${position!.latitude},${position.longitude})', + ), ]); } Future _setTitle(String? title) async { - await callMethod(this, 'setTitle', [title]); + await _bridge.callMethod(JsRef(toString()), 'setTitle', [title]); } Future _setZIndex(num? zIndex) async { - await callMethod(this, 'setZIndex', [zIndex]); + await _bridge.callMethod(JsRef(toString()), 'setZIndex', [zIndex]); } } @@ -316,14 +340,16 @@ class GMarker { /// map. class GPolyline { /// GPolyline Constructor. - GPolyline([GPolylineOptions? opts]) : id = _gid++ { + GPolyline(GoogleMapsJsBridge bridge, [GPolylineOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createPolyline(opts); } + final GoogleMapsJsBridge _bridge; + Future _createPolyline(GPolylineOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.Polyline($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.Polyline($opts)'); } /// GPolyline id. @@ -347,15 +373,20 @@ class GPolyline { } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GPolylineOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -397,7 +428,7 @@ class GPolylineOptions { } } - return '{geodesic:$geodesic, path:[$paths], strokeColor:"$strokeColor",' + return '{geodesic:$geodesic, path:[$paths], strokeColor:${jsonEncode(strokeColor)},' ' strokeOpacity:$strokeOpacity, map: map, strokeWeight:$strokeWeight, visible:$visible, zIndex:$zIndex}'; } } @@ -406,14 +437,16 @@ class GPolylineOptions { /// connected coordinates in an ordered sequence. class GPolygon { /// GPolygon Constructor. - GPolygon([GPolygonOptions? opts]) : id = _gid++ { + GPolygon(GoogleMapsJsBridge bridge, [GPolygonOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createPolygon(opts); } + final GoogleMapsJsBridge _bridge; + Future _createPolygon(GPolygonOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.Polygon($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.Polygon($opts)'); } /// GPolygon id. @@ -437,15 +470,20 @@ class GPolygon { } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GPolygonOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -497,8 +535,8 @@ class GPolygonOptions { str.write('], '); } - return '{fillColor:"$fillColor", fillOpacity:$fillOpacity, geodesic:$geodesic, paths:[$str],' - ' strokeColor:"$strokeColor", strokeOpacity:$strokeOpacity, map: map,' + return '{fillColor:${jsonEncode(fillColor)}, fillOpacity:$fillOpacity, geodesic:$geodesic, paths:[$str],' + ' strokeColor:${jsonEncode(strokeColor)}, strokeOpacity:$strokeOpacity, map: map,' ' strokeWeight:$strokeWeight, visible:$visible, zIndex:$zIndex}'; } } @@ -506,14 +544,16 @@ class GPolygonOptions { /// This class represents a circle using the passed GCircleOptions. class GCircle { /// GCircle Constructor. - GCircle([GCircleOptions? opts]) : id = _gid++ { + GCircle(GoogleMapsJsBridge bridge, [GCircleOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createCircle(opts); } + final GoogleMapsJsBridge _bridge; + Future _createCircle(GCircleOptions? opts) async { - await webController!.runJavaScript( - 'var ${toString()} = new google.maps.Circle($opts);', - ); + await _bridge.createObject(toString(), 'new google.maps.Circle($opts)'); } /// GCircle id. @@ -522,7 +562,7 @@ class GCircle { @override String toString() { - return 'polygon$id'; + return 'circle$id'; } /// Sets if the circle is visible. @@ -540,19 +580,22 @@ class GCircle { } Future _setVisible(bool? visible) async { - await callMethod(this, 'setVisible', [visible]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setRadius(num? radius) async { - await callMethod(this, 'setRadius', [radius]); + await _bridge.callMethod(JsRef(toString()), 'setRadius', [radius]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GCircleOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod(JsRef(toString()), 'setOptions', [ + options, + ]); } } @@ -590,8 +633,8 @@ class GCircleOptions { @override String toString() { - return '{center: new google.maps.LatLng(${center?.latitude}, ${center?.longitude}), fillColor:"$fillColor",' - ' fillOpacity:$fillOpacity, radius:$radius, strokeColor:"$strokeColor", strokeOpacity:$strokeOpacity,' + return '{center: new google.maps.LatLng(${center?.latitude}, ${center?.longitude}), fillColor:${jsonEncode(fillColor)},' + ' fillOpacity:$fillOpacity, radius:$radius, strokeColor:${jsonEncode(strokeColor)}, strokeOpacity:$strokeOpacity,' ' map: map, strokeWeight:$strokeWeight, visible:$visible, zIndex:$zIndex}'; } } @@ -599,74 +642,86 @@ class GCircleOptions { /// The [GMarkerClusterer] object used to cluster markers on the map. class GMarkerClusterer { /// GMarkerCluster Constructor. - GMarkerClusterer([GMarkerClustererOptions? opts]) : id = _gid++ { - _createMarkerClusterer(opts); + GMarkerClusterer(GoogleMapsJsBridge bridge, [GMarkerClustererOptions? opts]) + : _bridge = bridge, + id = _gid++ { + _ready = _createMarkerClusterer(opts); } - void _createMarkerClusterer(GMarkerClustererOptions? opts) { - final String command = - 'var ${toString()} = new markerClusterer.MarkerClusterer($opts);'; - webController!.runJavaScript(command); + final GoogleMapsJsBridge _bridge; + + /// Completes once the JS-side clusterer exists. Every method awaits this so + /// the ordering is stated in code rather than relying on the method channel + /// happening to deliver the constructor's script first. + late final Future _ready; + + Future _createMarkerClusterer(GMarkerClustererOptions? opts) async { + await _bridge.createObject( + toString(), + 'new markerClusterer.MarkerClusterer($opts)', + ); } /// GCircle id. final int id; static int _gid = 0; + JsRef get _ref => JsRef(toString()); + /// Adds a marker to be clustered by the [GMarkerClusterer]. - void addMarker(GMarker marker, bool? noDraw) { - webController!.runJavaScript('${toString()}.addMarker($marker, $noDraw);'); + Future addMarker(GMarker marker, bool? noDraw) async { + await _ready; + await _bridge.callMethod(_ref, 'addMarker', [ + JsRef(marker.toString()), + noDraw, + ]); } /// Removes a marker from the [GMarkerClusterer]. Future removeMarker(GMarker marker, bool? noDraw) async { - final bool result = await webController!.runJavaScriptReturningResult( - '${toString()}.removeMarker($marker, $noDraw);', - ) as bool; - - return result; - } - - /// Adds a list of markers to be clustered by the [GMarkerClusterer]. - void addMarkers(List? markers, bool? noDraw) { - final String command = - 'JSON.stringify($this.addMarkers.call($this, $markers, $noDraw))'; - webController!.runJavaScript(command); - } - - /// Removes a list of markers from the [GMarkerClusterer]. - bool removeMarkers(List? markers, bool? noDraw) { - final String command = - 'JSON.stringify($this.removeMarkers.call($this, $markers, $noDraw))'; - return webController!.runJavaScriptReturningResult(command) as bool; + await _ready; + final Object? result = await _bridge.callMethodReturning( + _ref, + 'removeMarker', + [JsRef(marker.toString()), noDraw], + ); + return result! as bool; } /// Clears all the markers from the [GMarkerClusterer]. - void clearMarkers(bool? noDraw) { - webController!.runJavaScript('${toString()}.clearMarkers($noDraw);'); + Future clearMarkers(bool? noDraw) async { + await _ready; + await _bridge.callMethod(_ref, 'clearMarkers', [noDraw]); } /// Returns the list of clusters. - List> get clusters { - final List> results = - webController!.runJavaScriptReturningResult('${toString()}.clusters') - as List>; - return results; + Future>> get clusters async { + await _ready; + final String value = + await _bridge.runJavaScriptReturningResult( + 'JSON.stringify(${toString()}.clusters)', + ) + as String; + final List results = json.decode(value) as List; + return results.cast>(); } /// Called when the [GMarkerClusterer] is added to the map. - void onAdd() { - webController!.runJavaScript('${toString()}.onAdd();'); + Future onAdd() async { + await _ready; + await _bridge.callMethod(_ref, 'onAdd', []); } /// Called when the [MarkerClusterer] is removed from the map. - void onRemove() { - webController!.runJavaScript('${toString()}.onRemove();'); + Future onRemove() async { + await _ready; + await _bridge.callMethod(_ref, 'onRemove', []); } /// Recalculates and draws all the marker clusters. - void render() { - webController!.runJavaScript('${toString()}.render();'); + Future render() async { + await _ready; + await _bridge.callMethod(_ref, 'render', []); } @override @@ -713,19 +768,23 @@ class GMarkerClustererOptions { /// against the Earth's surface. class GGroundOverlay { /// GGroundOverlay Constructor. - GGroundOverlay([GGroundOverlayOptions? opts]) - : id = _gid++, - _options = opts { + GGroundOverlay(GoogleMapsJsBridge bridge, [GGroundOverlayOptions? opts]) + : _bridge = bridge, + id = _gid++, + _options = opts { _createGroundOverlay(opts); } + final GoogleMapsJsBridge _bridge; + Future _createGroundOverlay(GGroundOverlayOptions? opts) async { - final String url = opts?.url ?? "''"; + final String url = jsonEncode(opts?.url ?? ''); final String bounds = opts?.bounds ?? '{}'; - final String command = - 'var ${toString()} = new google.maps.GroundOverlay($url, $bounds, $opts);' - ' ${toString()}.id = $id;'; - await webController!.runJavaScript(command); + final JsRef ref = await _bridge.createObject( + toString(), + 'new google.maps.GroundOverlay($url, $bounds, $opts)', + ); + await _bridge.setProperty(ref, 'id', id); } /// GGroundOverlay id. @@ -766,11 +825,12 @@ class GGroundOverlay { } Future _setMap(Object? map) async { - await callMethod(this, 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOpacity(num? opacity) async { - await callMethod(this, 'setOpacity', [opacity]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [opacity]); } } @@ -779,8 +839,7 @@ class GGroundOverlayOptions { /// GGroundOverlayOptions Constructor. GGroundOverlayOptions(); - /// The image URL passed to the JS GroundOverlay constructor, already quoted - /// as a JS string literal (e.g. `'data:image/png;base64,...'`). + /// The image URL passed to the JS GroundOverlay constructor. String? url; /// The bounds passed to the JS GroundOverlay constructor as a JS object @@ -802,18 +861,3 @@ class GGroundOverlayOptions { ' map: ${visible == false ? 'null' : 'map'}}'; } } - -/// Returns webview controller instance -WebViewController? webController; - -/// Sets the value to property of the object. -Future setProperty(Object o, String property, Object? value) async { - final String command = "JSON.stringify($o['$property'] = $value)"; - await webController!.runJavaScript(command); -} - -/// Calls the method of the object with the args. -Future callMethod(Object o, String method, List args) async { - final String command = 'JSON.stringify($o.$method.apply($o, $args))'; - await webController!.runJavaScript(command); -} diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 75b2e7220..6a04dd2a6 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter_tizen description: Tizen implementation of the google_maps_flutter plugin. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/google_maps_flutter -version: 0.1.14 +version: 0.2.0 environment: - sdk: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + sdk: ^3.8.0 + flutter: ">=3.32.0" flutter: plugin: @@ -19,5 +19,5 @@ dependencies: sdk: flutter google_maps_flutter_platform_interface: ^2.15.0 stream_transform: ^2.0.0 - webview_flutter: ^4.10.0 - webview_flutter_lwe: ^0.3.7 + webview_flutter: ^4.13.1 + webview_flutter_lwe: ^0.5.3