From 9037849fa21b4a7ff1aea6343a5709da053a0842 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 15 Jul 2026 15:26:50 +0900 Subject: [PATCH 01/12] [google_maps_flutter] Refactor JS bridge and verify integration tests based on upstream v2.17.0 --- .../lib/google_maps_flutter_tizen.dart | 2 +- .../google_maps_flutter/lib/src/circle.dart | 8 +- .../google_maps_flutter/lib/src/circles.dart | 13 +- .../lib/src/google_maps_controller.dart | 243 +++++----- .../lib/src/google_maps_js_bridge.dart | 427 ++++++++++++++++++ .../lib/src/ground_overlay.dart | 8 +- .../lib/src/ground_overlays.dart | 7 +- .../google_maps_flutter/lib/src/marker.dart | 10 +- .../lib/src/marker_clustering.dart | 5 + .../google_maps_flutter/lib/src/markers.dart | 10 +- .../google_maps_flutter/lib/src/polygon.dart | 8 +- .../google_maps_flutter/lib/src/polygons.dart | 13 +- .../google_maps_flutter/lib/src/polyline.dart | 8 +- .../lib/src/polylines.dart | 13 +- .../google_maps_flutter/lib/src/util.dart | 192 +++++--- packages/google_maps_flutter/pubspec.yaml | 4 +- 16 files changed, 715 insertions(+), 256 deletions(-) create mode 100644 packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart 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..52ee24d69 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, + required GoogleMapsJsBridge bridge, }) : _circle = circle, _consumeTapEvents = consumeTapEvents, tapEvent = onTap { - _addCircleEvent(controller); + _addCircleEvent(bridge); } util.GCircle? _circle; @@ -25,10 +25,10 @@ class CircleController { /// Circle component's tap event. ui.VoidCallback? tapEvent; - Future _addCircleEvent(WebViewController? controller) async { + Future _addCircleEvent(GoogleMapsJsBridge bridge) async { final String command = "$_circle.addListener('click', (event) => CircleClick.postMessage(JSON.stringify(${_circle?.id})));"; - await controller!.runJavaScript(command); + await bridge.runJavaScript(command); } /// 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..07127827c 100644 --- a/packages/google_maps_flutter/lib/src/circles.dart +++ b/packages/google_maps_flutter/lib/src/circles.dart @@ -8,8 +8,11 @@ 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, + CirclesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, _circleIdToController = {}, _idToCircleId = {}; @@ -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/google_maps_controller.dart b/packages/google_maps_flutter/lib/src/google_maps_controller.dart index 53e7c4ab8..f2d7559ca 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. @@ -34,18 +31,30 @@ class GoogleMapsController { _clusterManagers = clusterManagers, _groundOverlays = groundOverlays, _rawMapOptions = mapOptions { - _circlesController = CirclesController(stream: _streamController); - _polygonsController = PolygonsController(stream: _streamController); - _polylinesController = PolylinesController(stream: _streamController); + _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 = WebViewGoogleMapsJsBridge(); /// The Flutter widget that will contain the rendered Map. Used for caching. WebViewWidget? get webview => _webview; @@ -76,7 +85,7 @@ class GoogleMapsController { /// Returns min-max zoom levels. Test only. @visibleForTesting Future getMinMaxZoomLevels() async { - final String value = await controller.runJavaScriptReturningResult( + final String value = await _bridge.runJavaScriptReturningResult( 'JSON.stringify([map.minZoom, map.maxZoom])', ) as String; final dynamic bound = json.decode(value); @@ -100,7 +109,7 @@ class GoogleMapsController { /// Returns if zoomGestures property is enabled. Test only. @visibleForTesting Future isZoomGesturesEnabled() async { - final String value = await controller + final String value = await _bridge .runJavaScriptReturningResult('map.gestureHandling') as String; return value != 'none'; } @@ -108,15 +117,15 @@ class GoogleMapsController { /// 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 + final String value = await _bridge .runJavaScriptReturningResult('map.gestureHandling') as String; return value != 'none'; } @@ -128,88 +137,45 @@ class GoogleMapsController { } 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); + _bridge.events.listen(_onJsEvent); + _webview = WebViewWidget(controller: _bridge.controller); + } + + Future _onJsEvent(MapsJsEvent event) async { + switch (event) { + case BoundsChangedJsEvent(): + await _onBoundsChanged(); + case IdleJsEvent(): + _onIdle(); + case TilesLoadedJsEvent(): + _onTilesloaded(); + case ClickJsEvent(message: final String message): + _onClick(message); + case LongPressJsEvent(message: final String message): + _onLongPress(message); + case MarkerClickJsEvent(message: final String message): + _onMarkerClick(message); + case ClusterClickJsEvent(message: final String message): + _onClusterClick(message); + case MarkerDragStartJsEvent(message: final String message): + _onMarkerDragStart(message); + case MarkerDragJsEvent(message: final String message): + _onMarkerDrag(message); + case MarkerDragEndJsEvent(message: final String message): + _onMarkerDragEnd(message); + case PolylineClickJsEvent(message: final String message): + _onPolylineClick(message); + case PolygonClickJsEvent(message: final String message): + _onPolygonClick(message); + case CircleClickJsEvent(message: final String message): + _onCircleClick(message); + case GroundOverlayClickJsEvent(message: final String message): + _onGroundOverlayClick(message); + } } 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 += ']}}'; - - return result; - } - '''; - await controller.runJavaScript(command); + await _bridge.createMap(_createOptions()); } String _createOptions() { @@ -237,7 +203,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 +222,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 +239,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 +255,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 +271,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 +294,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 +310,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 +336,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 +362,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 +388,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 +405,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,9 +421,9 @@ class GoogleMapsController { } } - void _onCircleClick(JavaScriptMessage message) { + void _onCircleClick(String message) { try { - final dynamic id = json.decode(message.message); + final dynamic id = json.decode(message); if (_polygonsController != null && id is int) { final CircleId? circleId = _circlesController!._idToCircleId[id]; final CircleController? circle = @@ -471,9 +437,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 +464,7 @@ class GoogleMapsController { Future init() async { if (_webview == null && !_streamController.isClosed) { _getWebview(); - await _pageFinishedCompleter.future; + await _bridge.load(); await _createMap(); } await _attachGeometryControllers(); @@ -551,7 +517,6 @@ class GoogleMapsController { _clusterManagersController!.bindToMap(_mapId, _webview!); _groundOverlaysController!.bindToMap(_mapId, _webview!); - util.webController = controller; _controllersBoundToMap = true; } @@ -604,11 +569,11 @@ class GoogleMapsController { } Future _setOptions(String options) async { - await _callMethod(controller, 'setOptions', [options]); + await _callMethod('setOptions', [options]); } Future _setZoom(String options) async { - await _callMethod(controller, 'setZoom', [options]); + await _callMethod('setZoom', [options]); } // Attaches/detaches a Traffic Layer on the `map` if `attach` is true/false. @@ -626,39 +591,34 @@ 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', [options]); } Future _setPanTo(String options) async { - await _callMethod(controller, 'panTo', [options]); + await _callMethod('panTo', [options]); } Future _setPanBy(String options) async { - await _callMethod(controller, 'panBy', [options]); + await _callMethod('panBy', [options]); } Future _setFitBounds(String options) async { - await _callMethod(controller, 'fitBounds', [options]); + await _callMethod('fitBounds', [options]); } - Future _callMethod( - WebViewController controller, - String method, - List args, - ) async { - return controller.runJavaScriptReturningResult( + Future _callMethod(String method, List args) async { + return _bridge.runJavaScriptReturningResult( 'JSON.stringify(map.$method.apply(map, $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 +628,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, ); } @@ -768,7 +728,7 @@ class GoogleMapsController { JSON.stringify(getPixelToLatLng()); '''; - return await controller.runJavaScriptReturningResult(command) as String; + return await _bridge.runJavaScriptReturningResult(command) as String; } Future _latLngToPoint(LatLng latLng) async { @@ -788,12 +748,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 +861,7 @@ class GoogleMapsController { /// You won't be able to call many of the methods on this controller after /// calling `dispose`! void dispose() { + _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..6e0c75e2c --- /dev/null +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -0,0 +1,427 @@ +// Copyright 2021 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: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; +} + +/// Base class for events dispatched from the Google Maps JavaScript runtime +/// back into Dart through a [GoogleMapsJsBridge]. +abstract class MapsJsEvent { + /// Const constructor for subclasses. + const MapsJsEvent(); +} + +/// Fired when the map's `bounds_changed` listener fires. +class BoundsChangedJsEvent extends MapsJsEvent { + /// Creates a [BoundsChangedJsEvent]. + const BoundsChangedJsEvent(); +} + +/// Fired when the map's `idle` listener fires. +class IdleJsEvent extends MapsJsEvent { + /// Creates an [IdleJsEvent]. + const IdleJsEvent(); +} + +/// Fired when the map's `tilesloaded` listener fires. +class TilesLoadedJsEvent extends MapsJsEvent { + /// Creates a [TilesLoadedJsEvent]. + const TilesLoadedJsEvent(); +} + +/// Base class for [MapsJsEvent]s that carry a raw JSON [message] payload. +abstract class MessageJsEvent extends MapsJsEvent { + /// Creates a [MessageJsEvent] carrying the raw [message] payload. + const MessageJsEvent(this.message); + + /// The raw (JSON-encoded) message payload posted from the JS side. + final String message; +} + +/// Fired when the map is clicked. +class ClickJsEvent extends MessageJsEvent { + /// Creates a [ClickJsEvent]. + const ClickJsEvent(super.message); +} + +/// Fired when the map is long-pressed. +class LongPressJsEvent extends MessageJsEvent { + /// Creates a [LongPressJsEvent]. + const LongPressJsEvent(super.message); +} + +/// Fired when a marker is clicked. +class MarkerClickJsEvent extends MessageJsEvent { + /// Creates a [MarkerClickJsEvent]. + const MarkerClickJsEvent(super.message); +} + +/// Fired when a marker cluster is clicked. +class ClusterClickJsEvent extends MessageJsEvent { + /// Creates a [ClusterClickJsEvent]. + const ClusterClickJsEvent(super.message); +} + +/// Fired when a marker drag starts. +class MarkerDragStartJsEvent extends MessageJsEvent { + /// Creates a [MarkerDragStartJsEvent]. + const MarkerDragStartJsEvent(super.message); +} + +/// Fired while a marker is being dragged. +class MarkerDragJsEvent extends MessageJsEvent { + /// Creates a [MarkerDragJsEvent]. + const MarkerDragJsEvent(super.message); +} + +/// Fired when a marker drag ends. +class MarkerDragEndJsEvent extends MessageJsEvent { + /// Creates a [MarkerDragEndJsEvent]. + const MarkerDragEndJsEvent(super.message); +} + +/// Fired when a polyline is clicked. +class PolylineClickJsEvent extends MessageJsEvent { + /// Creates a [PolylineClickJsEvent]. + const PolylineClickJsEvent(super.message); +} + +/// Fired when a polygon is clicked. +class PolygonClickJsEvent extends MessageJsEvent { + /// Creates a [PolygonClickJsEvent]. + const PolygonClickJsEvent(super.message); +} + +/// Fired when a circle is clicked. +class CircleClickJsEvent extends MessageJsEvent { + /// Creates a [CircleClickJsEvent]. + const CircleClickJsEvent(super.message); +} + +/// Fired when a ground overlay is clicked. +class GroundOverlayClickJsEvent extends MessageJsEvent { + /// Creates a [GroundOverlayClickJsEvent]. + const GroundOverlayClickJsEvent(super.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. +abstract class GoogleMapsJsBridge { + /// The underlying WebView controller. Exposed so callers can build the + /// [WebViewWidget] that hosts this bridge's JS runtime. + WebViewController get controller; + + /// Broadcasts events received from the JS side. + Stream get events; + + /// Loads the map HTML shell and wires up the JS→Dart event channels. + /// + /// Completes once the page has finished loading. + Future load(); + + /// Creates the top-level `map` JS variable using [optionsJs] (a JS object + /// literal), plus its built-in map-level listeners. + Future createMap(String optionsJs); + + /// 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); + + /// Assigns `ref[property] = value` on the JS side. + Future setProperty(JsRef ref, String property, Object? value); + + /// Reads `ref.property` from the JS side. + Future getProperty(JsRef ref, String property); + + /// Calls `ref.method(...args)` on the JS side, discarding the result. + Future callMethod(JsRef ref, String method, List args); + + /// Calls `ref.method(...args)` on the JS side and returns the result. + Future callMethodReturning( + JsRef ref, + String method, + List args, + ); + + /// 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, + ); + + /// Escape hatch for JS not yet expressed in terms of the methods above. + Future runJavaScript(String script); + + /// Escape hatch for JS not yet expressed in terms of the methods above, + /// returning the raw result. + Future runJavaScriptReturningResult(String script); + + /// Releases the resources held by this bridge. + void dispose(); +} + +/// A [GoogleMapsJsBridge] backed by a [WebViewController]. +class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { + /// Creates a bridge over [controller], or a fresh [WebViewController] if + /// none is provided. + WebViewGoogleMapsJsBridge({WebViewController? controller}) + : controller = controller ?? WebViewController(); + + @override + final WebViewController controller; + + final StreamController _events = + StreamController.broadcast(); + final Completer _pageFinished = Completer(); + + @override + Stream get events => _events.stream; + + @override + 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) + ..addJavaScriptChannel( + 'BoundChanged', + onMessageReceived: (JavaScriptMessage message) { + _events.add(const BoundsChangedJsEvent()); + }, + ) + ..addJavaScriptChannel( + 'Idle', + onMessageReceived: (JavaScriptMessage message) { + _events.add(const IdleJsEvent()); + }, + ) + ..addJavaScriptChannel( + 'Tilesloaded', + onMessageReceived: (JavaScriptMessage message) { + _events.add(const TilesLoadedJsEvent()); + }, + ) + ..addJavaScriptChannel( + 'Click', + onMessageReceived: (JavaScriptMessage message) { + _events.add(ClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'LongPress', + onMessageReceived: (JavaScriptMessage message) { + _events.add(LongPressJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'ClusterClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(ClusterClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerDragStart', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerDragStartJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerDrag', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerDragJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'MarkerDragEnd', + onMessageReceived: (JavaScriptMessage message) { + _events.add(MarkerDragEndJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'PolylineClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(PolylineClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'PolygonClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(PolygonClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'CircleClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(CircleClickJsEvent(message.message)); + }, + ) + ..addJavaScriptChannel( + 'GroundOverlayClick', + onMessageReceived: (JavaScriptMessage message) { + _events.add(GroundOverlayClickJsEvent(message.message)); + }, + ) + ..loadFile(path); + + return _pageFinished.future; + } + + @override + 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); + } + + @override + Future createObject( + String varName, + String constructorExpression, + ) async { + await controller.runJavaScript('var $varName = $constructorExpression;'); + return JsRef(varName); + } + + @override + Future setProperty( + JsRef ref, + String property, + Object? value, + ) async { + await controller.runJavaScript( + "JSON.stringify($ref['$property'] = $value)", + ); + } + + @override + Future getProperty(JsRef ref, String property) async { + return controller.runJavaScriptReturningResult('$ref.$property'); + } + + @override + Future callMethod( + JsRef ref, + String method, + List args, + ) async { + await controller.runJavaScript( + 'JSON.stringify($ref.$method.apply($ref, $args))', + ); + } + + @override + Future callMethodReturning( + JsRef ref, + String method, + List args, + ) async { + return controller.runJavaScriptReturningResult( + '$ref.$method.apply($ref, $args)', + ); + } + + @override + Future addListener( + JsRef ref, + String eventName, + String channel, + String payloadJs, + ) async { + await controller.runJavaScript( + "$ref.addListener('$eventName', (event) => $channel.postMessage($payloadJs));", + ); + } + + @override + Future runJavaScript(String script) async { + await controller.runJavaScript(script); + } + + @override + Future runJavaScriptReturningResult(String script) async { + return controller.runJavaScriptReturningResult(script); + } + + @override + void dispose() { + _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..a0f329915 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, + required GoogleMapsJsBridge bridge, }) : _groundOverlay = groundOverlay, tapEvent = onTap { - _addGroundOverlayEvent(controller); + _addGroundOverlayEvent(bridge); } util.GGroundOverlay? _groundOverlay; @@ -24,10 +24,10 @@ class GroundOverlayController { /// Ground overlay component's tap event. ui.VoidCallback? tapEvent; - Future _addGroundOverlayEvent(WebViewController? controller) async { + Future _addGroundOverlayEvent(GoogleMapsJsBridge bridge) async { final String command = "$_groundOverlay.addListener('click', (event) => GroundOverlayClick.postMessage(JSON.stringify(${_groundOverlay?.id})));"; - await controller!.runJavaScript(command); + await bridge.runJavaScript(command); } /// 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..a7b8254e2 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlays.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlays.dart @@ -12,7 +12,9 @@ class GroundOverlaysController extends GeometryController { /// [GoogleMapController], and is shared with other controllers. GroundOverlaysController({ required StreamController> stream, + required GoogleMapsJsBridge bridge, }) : _streamController = stream, + _bridge = bridge, _groundOverlayIdToController = {}, _idToGroundOverlayId = {}; @@ -25,6 +27,8 @@ class GroundOverlaysController extends GeometryController { // 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..d5b23b534 100644 --- a/packages/google_maps_flutter/lib/src/marker.dart +++ b/packages/google_maps_flutter/lib/src/marker.dart @@ -17,7 +17,7 @@ class MarkerController { LatLngCallback? onDragEnd, ui.VoidCallback? onTap, ClusterManagerId? clusterManagerId, - WebViewController? controller, + GoogleMapsJsBridge? bridge, }) : _marker = marker, _infoWindow = infoWindow, _consumeTapEvents = consumeTapEvents, @@ -26,8 +26,8 @@ class MarkerController { dragStartEvent = onDragStart, dragEvent = onDrag, dragEndEvent = onDragEnd { - if (controller != null) { - _addMarkerEvent(controller); + if (bridge != null) { + _addMarkerEvent(bridge); } } @@ -49,13 +49,13 @@ class MarkerController { /// Marker component's drag end event. LatLngCallback? dragEndEvent; - Future _addMarkerEvent(WebViewController? controller) async { + Future _addMarkerEvent(GoogleMapsJsBridge bridge) 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); + await bridge.runJavaScript(command); } /// 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..dc3271cf9 100644 --- a/packages/google_maps_flutter/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/lib/src/marker_clustering.dart @@ -17,7 +17,9 @@ class ClusterManagersController extends GeometryController { /// emitting map events. ClusterManagersController({ required StreamController> stream, + required GoogleMapsJsBridge bridge, }) : _streamController = stream, + _bridge = bridge, _idToClusterManagerId = {}, _clusterManagerIdToMarkerClusterer = {}; @@ -25,6 +27,8 @@ class ClusterManagersController extends GeometryController { // 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; @@ -48,6 +52,7 @@ class ClusterManagersController extends GeometryController { ); final util.GMarkerClusterer markerClusterer = util.GMarkerClusterer( + _bridge, options, ); diff --git a/packages/google_maps_flutter/lib/src/markers.dart b/packages/google_maps_flutter/lib/src/markers.dart index 41b5e7450..6595c962a 100644 --- a/packages/google_maps_flutter/lib/src/markers.dart +++ b/packages/google_maps_flutter/lib/src/markers.dart @@ -11,8 +11,10 @@ class MarkersController extends GeometryController { MarkersController({ required StreamController> stream, required ClusterManagersController clusterManagersController, + required GoogleMapsJsBridge bridge, }) : _streamController = stream, _clusterManagersController = clusterManagersController, + _bridge = bridge, _idToMarkerId = {}, _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; diff --git a/packages/google_maps_flutter/lib/src/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart index e15d70e43..217a49249 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, + required GoogleMapsJsBridge bridge, }) : _polygon = polygon, _consumeTapEvents = consumeTapEvents, tapEvent = onTap { - _addPolygonEvent(controller); + _addPolygonEvent(bridge); } util.GPolygon? _polygon; @@ -25,10 +25,10 @@ class PolygonController { /// Polygon component's tap event. ui.VoidCallback? tapEvent; - Future _addPolygonEvent(WebViewController? controller) async { + Future _addPolygonEvent(GoogleMapsJsBridge bridge) async { final String command = "$_polygon.addListener('click', (event) => PolygonClick.postMessage(JSON.stringify(${_polygon?.id})));"; - await controller!.runJavaScript(command); + await bridge.runJavaScript(command); } /// 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..4fd1f2222 100644 --- a/packages/google_maps_flutter/lib/src/polygons.dart +++ b/packages/google_maps_flutter/lib/src/polygons.dart @@ -8,8 +8,11 @@ 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, + PolygonsController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, _polygonIdToController = {}, _idToPolygonId = {}; @@ -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..9269eadc0 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, + required GoogleMapsJsBridge bridge, }) : _polyline = polyline, _consumeTapEvents = consumeTapEvents, tapEvent = onTap { - _addPolylineEvent(controller); + _addPolylineEvent(bridge); } util.GPolyline? _polyline; @@ -25,10 +25,10 @@ class PolylineController { /// Polyline component's tap event. ui.VoidCallback? tapEvent; - Future _addPolylineEvent(WebViewController? controller) async { + Future _addPolylineEvent(GoogleMapsJsBridge bridge) async { final String command = "$_polyline.addListener('click', (event) => PolylineClick.postMessage(JSON.stringify(${_polyline?.id})));"; - await controller!.runJavaScript(command); + await bridge.runJavaScript(command); } /// 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..94973d36a 100644 --- a/packages/google_maps_flutter/lib/src/polylines.dart +++ b/packages/google_maps_flutter/lib/src/polylines.dart @@ -8,8 +8,11 @@ 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, + PolylinesController({ + required StreamController> stream, + required GoogleMapsJsBridge bridge, + }) : _streamController = stream, + _bridge = bridge, _polylineIdToController = {}, _idToPolylineId = {}; @@ -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..4031dd5af 100644 --- a/packages/google_maps_flutter/lib/src/util.dart +++ b/packages/google_maps_flutter/lib/src/util.dart @@ -7,7 +7,8 @@ import 'dart:async'; 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); @@ -148,13 +149,18 @@ class GInfoWindowOptions { /// 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)', ); } @@ -167,7 +173,7 @@ class GInfoWindow { } Future _callCloseInfoWindow() async { - await webController!.runJavaScript('${toString()}.close();'); + await _bridge.runJavaScript('${toString()}.close();'); } /// Opens InfoWindow on the given map. @@ -176,9 +182,7 @@ class GInfoWindow { } Future _callOpenInfoWindow(GMarker? anchor) async { - await webController!.runJavaScript( - '${toString()}.open({anchor: $anchor, map});', - ); + await _bridge.runJavaScript('${toString()}.open({anchor: $anchor, map});'); } @override @@ -193,27 +197,36 @@ 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?.toValue(), + ); } } /// This class represents a geographical location on the map as a Marker. class GMarker { /// GMarker Constructor. - GMarker([GMarkerOptions? opts]) - : id = _gid++, + 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); + await _bridge.runJavaScript(command); } /// GMarker id. @@ -274,41 +287,49 @@ class GMarker { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await callMethod(this, 'setMap', [map]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } 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', [ + await _bridge.callMethod(JsRef(toString()), 'setPosition', [ '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,13 +337,18 @@ 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)', ); } @@ -347,15 +373,21 @@ 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]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOptions(GPolylineOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -406,14 +438,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 +471,21 @@ 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]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOptions(GPolygonOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -506,14 +546,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 +564,7 @@ class GCircle { @override String toString() { - return 'polygon$id'; + return 'circle$id'; } /// Sets if the circle is visible. @@ -540,19 +582,25 @@ 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]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOptions(GCircleOptions? options) async { - await callMethod(this, 'setOptions', [options]); + await _bridge.callMethod( + JsRef(toString()), + 'setOptions', + [options], + ); } } @@ -599,14 +647,18 @@ class GCircleOptions { /// The [GMarkerClusterer] object used to cluster markers on the map. class GMarkerClusterer { /// GMarkerCluster Constructor. - GMarkerClusterer([GMarkerClustererOptions? opts]) : id = _gid++ { + GMarkerClusterer(GoogleMapsJsBridge bridge, [GMarkerClustererOptions? opts]) + : _bridge = bridge, + id = _gid++ { _createMarkerClusterer(opts); } + final GoogleMapsJsBridge _bridge; + void _createMarkerClusterer(GMarkerClustererOptions? opts) { final String command = 'var ${toString()} = new markerClusterer.MarkerClusterer($opts);'; - webController!.runJavaScript(command); + _bridge.runJavaScript(command); } /// GCircle id. @@ -615,12 +667,12 @@ class GMarkerClusterer { /// Adds a marker to be clustered by the [GMarkerClusterer]. void addMarker(GMarker marker, bool? noDraw) { - webController!.runJavaScript('${toString()}.addMarker($marker, $noDraw);'); + _bridge.runJavaScript('${toString()}.addMarker($marker, $noDraw);'); } /// Removes a marker from the [GMarkerClusterer]. Future removeMarker(GMarker marker, bool? noDraw) async { - final bool result = await webController!.runJavaScriptReturningResult( + final bool result = await _bridge.runJavaScriptReturningResult( '${toString()}.removeMarker($marker, $noDraw);', ) as bool; @@ -631,42 +683,42 @@ class GMarkerClusterer { void addMarkers(List? markers, bool? noDraw) { final String command = 'JSON.stringify($this.addMarkers.call($this, $markers, $noDraw))'; - webController!.runJavaScript(command); + _bridge.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; + return _bridge.runJavaScriptReturningResult(command) as bool; } /// Clears all the markers from the [GMarkerClusterer]. void clearMarkers(bool? noDraw) { - webController!.runJavaScript('${toString()}.clearMarkers($noDraw);'); + _bridge.runJavaScript('${toString()}.clearMarkers($noDraw);'); } /// Returns the list of clusters. List> get clusters { final List> results = - webController!.runJavaScriptReturningResult('${toString()}.clusters') + _bridge.runJavaScriptReturningResult('${toString()}.clusters') as List>; return results; } /// Called when the [GMarkerClusterer] is added to the map. void onAdd() { - webController!.runJavaScript('${toString()}.onAdd();'); + _bridge.runJavaScript('${toString()}.onAdd();'); } /// Called when the [MarkerClusterer] is removed from the map. void onRemove() { - webController!.runJavaScript('${toString()}.onRemove();'); + _bridge.runJavaScript('${toString()}.onRemove();'); } /// Recalculates and draws all the marker clusters. void render() { - webController!.runJavaScript('${toString()}.render();'); + _bridge.runJavaScript('${toString()}.render();'); } @override @@ -713,19 +765,22 @@ class GMarkerClustererOptions { /// against the Earth's surface. class GGroundOverlay { /// GGroundOverlay Constructor. - GGroundOverlay([GGroundOverlayOptions? opts]) - : id = _gid++, + 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 bounds = opts?.bounds ?? '{}'; final String command = 'var ${toString()} = new google.maps.GroundOverlay($url, $bounds, $opts);' ' ${toString()}.id = $id;'; - await webController!.runJavaScript(command); + await _bridge.runJavaScript(command); } /// GGroundOverlay id. @@ -766,11 +821,13 @@ class GGroundOverlay { } Future _setMap(Object? map) async { - await callMethod(this, 'setMap', [map]); + await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); } Future _setOpacity(num? opacity) async { - await callMethod(this, 'setOpacity', [opacity]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [ + opacity, + ]); } } @@ -802,18 +859,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..0dfa3ad8a 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -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.0 From 08a1f78c4ac1293de4852931b642161e36dc6577 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Wed, 15 Jul 2026 21:01:41 +0900 Subject: [PATCH 02/12] [google_maps_flutter] Bump version to 0.1.15 --- packages/google_maps_flutter/CHANGELOG.md | 7 +++++++ packages/google_maps_flutter/README.md | 2 +- packages/google_maps_flutter/pubspec.yaml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index c14478638..4c4b3b6e1 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.1.15 + +* Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, + replacing direct `WebViewController` calls. +* Update `webview_flutter` to ^4.13.1 and `webview_flutter_lwe` to ^0.5.0. +* 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..285e729a5 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.1.15 ``` For detailed usage, see https://pub.dev/packages/google_maps_flutter#sample-usage. diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 0dfa3ad8a..820ec89da 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ 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.1.15 environment: sdk: ">=3.4.0 <4.0.0" From e44f1da910ec51c4e90288d03aa867781ede7496 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Thu, 16 Jul 2026 09:54:57 +0900 Subject: [PATCH 03/12] [google_maps_flutter] Fix unsafe JS argument serialization and use addListener consistently Address Gemini Code Assist review on PR #1066. - Fix a critical bug where callMethod/setProperty interpolated arguments via List.toString(), so a plain String like a marker title was spliced into the JS snippet unquoted (e.g. ['My Title'] instead of ["My Title"]), breaking at runtime for any string containing whitespace or special characters and risking JS injection. Add JsExpression to mark raw JS code that must not be quoted, and JSON-encode plain String arguments so they're safely treated as string literals. - Wrap the few call sites that relied on the old unquoted behavior (setMap's 'map' JS-global reference, setPosition's LatLng constructor, setPixelOffset's GSize object literal) in JsExpression so they keep working under the new serialization. - Mark MapsJsEvent sealed so the exhaustive switch in GoogleMapsController._onJsEvent is statically checked when new event types are added. - Use the bridge.addListener abstraction in polyline/circle/polygon/ ground_overlay instead of hand-built runJavaScript strings, for consistency with the rest of the bridge. --- .../google_maps_flutter/lib/src/circle.dart | 9 +++-- .../lib/src/google_maps_js_bridge.dart | 40 +++++++++++++++++-- .../lib/src/ground_overlay.dart | 9 +++-- .../google_maps_flutter/lib/src/polygon.dart | 9 +++-- .../google_maps_flutter/lib/src/polyline.dart | 9 +++-- .../google_maps_flutter/lib/src/util.dart | 21 ++++++---- 6 files changed, 74 insertions(+), 23 deletions(-) diff --git a/packages/google_maps_flutter/lib/src/circle.dart b/packages/google_maps_flutter/lib/src/circle.dart index 52ee24d69..65dcd3405 100644 --- a/packages/google_maps_flutter/lib/src/circle.dart +++ b/packages/google_maps_flutter/lib/src/circle.dart @@ -26,9 +26,12 @@ class CircleController { ui.VoidCallback? tapEvent; Future _addCircleEvent(GoogleMapsJsBridge bridge) async { - final String command = - "$_circle.addListener('click', (event) => CircleClick.postMessage(JSON.stringify(${_circle?.id})));"; - await bridge.runJavaScript(command); + 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/google_maps_js_bridge.dart b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart index 6e0c75e2c..f058202d9 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -4,6 +4,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:webview_flutter/webview_flutter.dart'; @@ -28,9 +29,22 @@ class JsRef { 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; +} + /// Base class for events dispatched from the Google Maps JavaScript runtime /// back into Dart through a [GoogleMapsJsBridge]. -abstract class MapsJsEvent { +sealed class MapsJsEvent { /// Const constructor for subclasses. const MapsJsEvent(); } @@ -360,6 +374,22 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { 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(); + } + @override Future setProperty( JsRef ref, @@ -367,7 +397,7 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { Object? value, ) async { await controller.runJavaScript( - "JSON.stringify($ref['$property'] = $value)", + "JSON.stringify($ref['$property'] = ${_serializeArg(value)})", ); } @@ -382,8 +412,9 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { String method, List args, ) async { + final String serializedArgs = '[${args.map(_serializeArg).join(', ')}]'; await controller.runJavaScript( - 'JSON.stringify($ref.$method.apply($ref, $args))', + 'JSON.stringify($ref.$method.apply($ref, $serializedArgs))', ); } @@ -393,8 +424,9 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { String method, List args, ) async { + final String serializedArgs = '[${args.map(_serializeArg).join(', ')}]'; return controller.runJavaScriptReturningResult( - '$ref.$method.apply($ref, $args)', + '$ref.$method.apply($ref, $serializedArgs)', ); } diff --git a/packages/google_maps_flutter/lib/src/ground_overlay.dart b/packages/google_maps_flutter/lib/src/ground_overlay.dart index a0f329915..0f125abac 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlay.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlay.dart @@ -25,9 +25,12 @@ class GroundOverlayController { ui.VoidCallback? tapEvent; Future _addGroundOverlayEvent(GoogleMapsJsBridge bridge) async { - final String command = - "$_groundOverlay.addListener('click', (event) => GroundOverlayClick.postMessage(JSON.stringify(${_groundOverlay?.id})));"; - await bridge.runJavaScript(command); + 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/polygon.dart b/packages/google_maps_flutter/lib/src/polygon.dart index 217a49249..0997f976b 100644 --- a/packages/google_maps_flutter/lib/src/polygon.dart +++ b/packages/google_maps_flutter/lib/src/polygon.dart @@ -26,9 +26,12 @@ class PolygonController { ui.VoidCallback? tapEvent; Future _addPolygonEvent(GoogleMapsJsBridge bridge) async { - final String command = - "$_polygon.addListener('click', (event) => PolygonClick.postMessage(JSON.stringify(${_polygon?.id})));"; - await bridge.runJavaScript(command); + 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/polyline.dart b/packages/google_maps_flutter/lib/src/polyline.dart index 9269eadc0..cb798d0a6 100644 --- a/packages/google_maps_flutter/lib/src/polyline.dart +++ b/packages/google_maps_flutter/lib/src/polyline.dart @@ -26,9 +26,12 @@ class PolylineController { ui.VoidCallback? tapEvent; Future _addPolylineEvent(GoogleMapsJsBridge bridge) async { - final String command = - "$_polyline.addListener('click', (event) => PolylineClick.postMessage(JSON.stringify(${_polyline?.id})));"; - await bridge.runJavaScript(command); + 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/util.dart b/packages/google_maps_flutter/lib/src/util.dart index 4031dd5af..7db064cdc 100644 --- a/packages/google_maps_flutter/lib/src/util.dart +++ b/packages/google_maps_flutter/lib/src/util.dart @@ -206,7 +206,7 @@ class GInfoWindow { await _bridge.setProperty( JsRef(toString()), 'pixelOffset', - size?.toValue(), + size != null ? JsExpression(size.toValue()) : null, ); } } @@ -287,7 +287,8 @@ class GMarker { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GMarkerOptions? options) async { @@ -320,7 +321,9 @@ class GMarker { Future _setPosition(LatLng? position) async { await _bridge.callMethod(JsRef(toString()), 'setPosition', [ - 'new google.maps.LatLng(${position!.latitude},${position.longitude})', + JsExpression( + 'new google.maps.LatLng(${position!.latitude},${position.longitude})', + ), ]); } @@ -379,7 +382,8 @@ class GPolyline { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GPolylineOptions? options) async { @@ -477,7 +481,8 @@ class GPolygon { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GPolygonOptions? options) async { @@ -592,7 +597,8 @@ class GCircle { } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { - await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOptions(GCircleOptions? options) async { @@ -821,7 +827,8 @@ class GGroundOverlay { } Future _setMap(Object? map) async { - await _bridge.callMethod(JsRef(toString()), 'setMap', [map]); + final Object? mapArg = map is String ? JsExpression(map) : map; + await _bridge.callMethod(JsRef(toString()), 'setMap', [mapArg]); } Future _setOpacity(num? opacity) async { From 6ac214772f044de299275dbc1a7ae34244e1eb3f Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 21 Jul 2026 17:56:56 +0900 Subject: [PATCH 04/12] [google_maps_flutter] Update webview_flutter_lwe to 0.5.2 --- packages/google_maps_flutter/CHANGELOG.md | 2 +- packages/google_maps_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 4c4b3b6e1..f1bcfe474 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -2,7 +2,7 @@ * Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, replacing direct `WebViewController` calls. -* Update `webview_flutter` to ^4.13.1 and `webview_flutter_lwe` to ^0.5.0. +* Update `webview_flutter_lwe` to ^0.5.2. * Verify integration tests pass against upstream google_maps_flutter v2.17.0. ## 0.1.14 diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 820ec89da..e1972044b 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -20,4 +20,4 @@ dependencies: google_maps_flutter_platform_interface: ^2.15.0 stream_transform: ^2.0.0 webview_flutter: ^4.13.1 - webview_flutter_lwe: ^0.5.0 + webview_flutter_lwe: ^0.5.2 From de583b5710e0ac1ce043284deaff273fb9038ed5 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 21 Jul 2026 18:04:33 +0900 Subject: [PATCH 05/12] [google_maps_flutter] JSON-encode InfoWindow content and seal MessageJsEvent Follow-up to 8b9c3079: GInfoWindowOptions.toString() still spliced the info window content string directly into the JS snippet unquoted, the same unsafe-serialization issue fixed elsewhere in that commit. Also mark MessageJsEvent sealed, matching MapsJsEvent, so exhaustive switches over its subclasses stay statically checked. --- .../google_maps_flutter/lib/src/google_maps_js_bridge.dart | 2 +- packages/google_maps_flutter/lib/src/util.dart | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 index f058202d9..042078108 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -68,7 +68,7 @@ class TilesLoadedJsEvent extends MapsJsEvent { } /// Base class for [MapsJsEvent]s that carry a raw JSON [message] payload. -abstract class MessageJsEvent extends MapsJsEvent { +sealed class MessageJsEvent extends MapsJsEvent { /// Creates a [MessageJsEvent] carrying the raw [message] payload. const MessageJsEvent(this.message); diff --git a/packages/google_maps_flutter/lib/src/util.dart b/packages/google_maps_flutter/lib/src/util.dart index 7db064cdc..57cd3c4e4 100644 --- a/packages/google_maps_flutter/lib/src/util.dart +++ b/packages/google_maps_flutter/lib/src/util.dart @@ -6,6 +6,8 @@ // 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 'google_maps_js_bridge.dart'; @@ -142,7 +144,8 @@ 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}'; } } From e8d9e43a34c78094cb62211c9f10e3e75048faf8 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Thu, 6 Aug 2026 09:47:45 +0900 Subject: [PATCH 06/12] [google_maps_flutter] Update webveiw_flutter_lwe version(0.5.3) --- packages/google_maps_flutter/CHANGELOG.md | 2 +- packages/google_maps_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index f1bcfe474..08acaf164 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -2,7 +2,7 @@ * Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, replacing direct `WebViewController` calls. -* Update `webview_flutter_lwe` to ^0.5.2. +* Update `webview_flutter_lwe` to ^0.5.3. * Verify integration tests pass against upstream google_maps_flutter v2.17.0. ## 0.1.14 diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index e1972044b..5e63b5d2b 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -20,4 +20,4 @@ dependencies: google_maps_flutter_platform_interface: ^2.15.0 stream_transform: ^2.0.0 webview_flutter: ^4.13.1 - webview_flutter_lwe: ^0.5.2 + webview_flutter_lwe: ^0.5.3 From a331524bbcd2e42da58fddfef4f516d2430aff28 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Mon, 10 Aug 2026 17:49:27 +0900 Subject: [PATCH 07/12] [google_maps_flutter] Raise SDK constraint to match webview_flutter_lwe 0.5.3 webview_flutter_lwe 0.5.3 requires Dart ^3.8.0 and Flutter >=3.32.0, but this package still advertised Dart >=3.4.0 and Flutter >=3.22.0. Consumers on the lower range would fail dependency resolution since the only version satisfying webview_flutter_lwe ^0.5.3 needs the higher SDKs. Address chatgpt-codex-connector review on PR #1066. --- packages/google_maps_flutter/CHANGELOG.md | 2 ++ packages/google_maps_flutter/pubspec.yaml | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 08acaf164..c1d92c925 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -3,6 +3,8 @@ * Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, replacing direct `WebViewController` calls. * Update `webview_flutter_lwe` to ^0.5.3. +* Raise the SDK constraint to match `webview_flutter_lwe` 0.5.3 + (Dart ^3.8.0, Flutter >=3.32.0). * Verify integration tests pass against upstream google_maps_flutter v2.17.0. ## 0.1.14 diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 5e63b5d2b..6b4d675bb 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -5,8 +5,8 @@ repository: https://github.com/flutter-tizen/plugins/tree/master/packages/google version: 0.1.15 environment: - sdk: ">=3.4.0 <4.0.0" - flutter: ">=3.22.0" + sdk: ^3.8.0 + flutter: ">=3.32.0" flutter: plugin: From e36835317d0771d797bf8ea0f01894095b5b8015 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Mon, 10 Aug 2026 17:49:32 +0900 Subject: [PATCH 08/12] [google_maps_flutter] Guard JS channel callbacks after bridge disposal dispose() closed the _events StreamController but left the JavaScript channels registered, so a message from an in-flight JS timer or postMessage call arriving after disposal would call _events.add() on a closed controller and throw an uncaught StateError. Route all channel callbacks through a helper that ignores emissions once the controller is closed. Address chatgpt-codex-connector review on PR #1066. --- .../lib/src/google_maps_js_bridge.dart | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) 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 index 042078108..6e05d8341 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -223,6 +223,18 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { @override 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); + } + } + @override Future load() { String path = Platform.environment['AUL_ROOT_PATH'] ?? ''; @@ -241,85 +253,85 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { ..addJavaScriptChannel( 'BoundChanged', onMessageReceived: (JavaScriptMessage message) { - _events.add(const BoundsChangedJsEvent()); + _emit(const BoundsChangedJsEvent()); }, ) ..addJavaScriptChannel( 'Idle', onMessageReceived: (JavaScriptMessage message) { - _events.add(const IdleJsEvent()); + _emit(const IdleJsEvent()); }, ) ..addJavaScriptChannel( 'Tilesloaded', onMessageReceived: (JavaScriptMessage message) { - _events.add(const TilesLoadedJsEvent()); + _emit(const TilesLoadedJsEvent()); }, ) ..addJavaScriptChannel( 'Click', onMessageReceived: (JavaScriptMessage message) { - _events.add(ClickJsEvent(message.message)); + _emit(ClickJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'LongPress', onMessageReceived: (JavaScriptMessage message) { - _events.add(LongPressJsEvent(message.message)); + _emit(LongPressJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'MarkerClick', onMessageReceived: (JavaScriptMessage message) { - _events.add(MarkerClickJsEvent(message.message)); + _emit(MarkerClickJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'ClusterClick', onMessageReceived: (JavaScriptMessage message) { - _events.add(ClusterClickJsEvent(message.message)); + _emit(ClusterClickJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'MarkerDragStart', onMessageReceived: (JavaScriptMessage message) { - _events.add(MarkerDragStartJsEvent(message.message)); + _emit(MarkerDragStartJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'MarkerDrag', onMessageReceived: (JavaScriptMessage message) { - _events.add(MarkerDragJsEvent(message.message)); + _emit(MarkerDragJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'MarkerDragEnd', onMessageReceived: (JavaScriptMessage message) { - _events.add(MarkerDragEndJsEvent(message.message)); + _emit(MarkerDragEndJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'PolylineClick', onMessageReceived: (JavaScriptMessage message) { - _events.add(PolylineClickJsEvent(message.message)); + _emit(PolylineClickJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'PolygonClick', onMessageReceived: (JavaScriptMessage message) { - _events.add(PolygonClickJsEvent(message.message)); + _emit(PolygonClickJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'CircleClick', onMessageReceived: (JavaScriptMessage message) { - _events.add(CircleClickJsEvent(message.message)); + _emit(CircleClickJsEvent(message.message)); }, ) ..addJavaScriptChannel( 'GroundOverlayClick', onMessageReceived: (JavaScriptMessage message) { - _events.add(GroundOverlayClickJsEvent(message.message)); + _emit(GroundOverlayClickJsEvent(message.message)); }, ) ..loadFile(path); From c13f8ba6a1069216b21db7693a27b8d5bd6a68f9 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Mon, 10 Aug 2026 18:36:42 +0900 Subject: [PATCH 09/12] [google_maps_flutter] Reformat lib/src with dart's tall-style formatter Raising the SDK constraint to ^3.8.0 activates the new tall-style dart format output, which the CI format check enforces. --- .../google_maps_flutter/lib/src/circle.dart | 6 +- .../google_maps_flutter/lib/src/circles.dart | 8 +- .../google_maps_flutter/lib/src/convert.dart | 41 +++++----- .../lib/src/google_maps_controller.dart | 52 +++++++----- .../lib/src/google_maps_js_bridge.dart | 17 ++-- .../lib/src/ground_overlay.dart | 4 +- .../lib/src/ground_overlays.dart | 12 +-- .../google_maps_flutter/lib/src/marker.dart | 19 +++-- .../lib/src/marker_clustering.dart | 12 +-- .../google_maps_flutter/lib/src/markers.dart | 19 +++-- .../google_maps_flutter/lib/src/polygon.dart | 6 +- .../google_maps_flutter/lib/src/polygons.dart | 8 +- .../google_maps_flutter/lib/src/polyline.dart | 6 +- .../lib/src/polylines.dart | 8 +- .../google_maps_flutter/lib/src/util.dart | 82 ++++++++----------- 15 files changed, 144 insertions(+), 156 deletions(-) diff --git a/packages/google_maps_flutter/lib/src/circle.dart b/packages/google_maps_flutter/lib/src/circle.dart index 65dcd3405..4a7cea7f3 100644 --- a/packages/google_maps_flutter/lib/src/circle.dart +++ b/packages/google_maps_flutter/lib/src/circle.dart @@ -13,9 +13,9 @@ class CircleController { bool consumeTapEvents = false, ui.VoidCallback? onTap, required GoogleMapsJsBridge bridge, - }) : _circle = circle, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { + }) : _circle = circle, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { _addCircleEvent(bridge); } diff --git a/packages/google_maps_flutter/lib/src/circles.dart b/packages/google_maps_flutter/lib/src/circles.dart index 07127827c..9f0a22a4a 100644 --- a/packages/google_maps_flutter/lib/src/circles.dart +++ b/packages/google_maps_flutter/lib/src/circles.dart @@ -11,10 +11,10 @@ class CirclesController extends GeometryController { CirclesController({ required StreamController> stream, required GoogleMapsJsBridge bridge, - }) : _streamController = stream, - _bridge = bridge, - _circleIdToController = {}, - _idToCircleId = {}; + }) : _streamController = stream, + _bridge = bridge, + _circleIdToController = {}, + _idToCircleId = {}; // A cache of [CircleController]s indexed by their [CircleId]. final Map _circleIdToController; diff --git a/packages/google_maps_flutter/lib/src/convert.dart b/packages/google_maps_flutter/lib/src/convert.dart index 0e7d3b37a..03d5c1ef1 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'); } @@ -461,7 +462,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)); } @@ -506,7 +508,8 @@ util.GGroundOverlayOptions? _groundOverlayOptionsFromGroundOverlay( } return util.GGroundOverlayOptions() ..url = "'$imageUrl'" - ..bounds = '{south:${bounds.southwest.latitude},' + ..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 f2d7559ca..931e3583b 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_controller.dart @@ -21,16 +21,16 @@ 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 { + }) : _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, @@ -85,9 +85,11 @@ class GoogleMapsController { /// Returns min-max zoom levels. Test only. @visibleForTesting Future getMinMaxZoomLevels() async { - final String value = await _bridge.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) { @@ -109,8 +111,9 @@ class GoogleMapsController { /// Returns if zoomGestures property is enabled. Test only. @visibleForTesting Future isZoomGesturesEnabled() async { - final String value = await _bridge - .runJavaScriptReturningResult('map.gestureHandling') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.gestureHandling') + as String; return value != 'none'; } @@ -125,8 +128,9 @@ class GoogleMapsController { /// Returns if scrollGestures property is enabled. Test only. @visibleForTesting Future isScrollGesturesEnabled() async { - final String value = await _bridge - .runJavaScriptReturningResult('map.gestureHandling') as String; + final String value = + await _bridge.runJavaScriptReturningResult('map.gestureHandling') + as String; return value != 'none'; } @@ -578,7 +582,8 @@ class GoogleMapsController { // 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(); @@ -685,8 +690,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 { @@ -714,7 +720,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(); @@ -732,7 +739,8 @@ class GoogleMapsController { } Future _latLngToPoint(LatLng latLng) async { - final String command = ''' + final String command = + ''' function getLatLngToPixel() { var ne = map.getBounds().getNorthEast(); var sw = map.getBounds().getSouthWest(); 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 index 6e05d8341..738fd5e40 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -211,7 +211,7 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { /// Creates a bridge over [controller], or a fresh [WebViewController] if /// none is provided. WebViewGoogleMapsJsBridge({WebViewController? controller}) - : controller = controller ?? WebViewController(); + : controller = controller ?? WebViewController(); @override final WebViewController controller; @@ -341,7 +341,8 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { @override Future createMap(String optionsJs) async { - final String command = ''' + 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(''); }); @@ -403,11 +404,7 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { } @override - Future setProperty( - JsRef ref, - String property, - Object? value, - ) async { + Future setProperty(JsRef ref, String property, Object? value) async { await controller.runJavaScript( "JSON.stringify($ref['$property'] = ${_serializeArg(value)})", ); @@ -419,11 +416,7 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { } @override - Future callMethod( - JsRef ref, - String method, - List args, - ) async { + 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))', diff --git a/packages/google_maps_flutter/lib/src/ground_overlay.dart b/packages/google_maps_flutter/lib/src/ground_overlay.dart index 0f125abac..0414e909b 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlay.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlay.dart @@ -14,8 +14,8 @@ class GroundOverlayController { required util.GGroundOverlay groundOverlay, ui.VoidCallback? onTap, required GoogleMapsJsBridge bridge, - }) : _groundOverlay = groundOverlay, - tapEvent = onTap { + }) : _groundOverlay = groundOverlay, + tapEvent = onTap { _addGroundOverlayEvent(bridge); } diff --git a/packages/google_maps_flutter/lib/src/ground_overlays.dart b/packages/google_maps_flutter/lib/src/ground_overlays.dart index a7b8254e2..2d89d16e7 100644 --- a/packages/google_maps_flutter/lib/src/ground_overlays.dart +++ b/packages/google_maps_flutter/lib/src/ground_overlays.dart @@ -13,15 +13,15 @@ class GroundOverlaysController extends GeometryController { GroundOverlaysController({ required StreamController> stream, required GoogleMapsJsBridge bridge, - }) : _streamController = stream, - _bridge = bridge, - _groundOverlayIdToController = - {}, - _idToGroundOverlayId = {}; + }) : _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. diff --git a/packages/google_maps_flutter/lib/src/marker.dart b/packages/google_maps_flutter/lib/src/marker.dart index d5b23b534..bbc24b3ff 100644 --- a/packages/google_maps_flutter/lib/src/marker.dart +++ b/packages/google_maps_flutter/lib/src/marker.dart @@ -18,14 +18,14 @@ class MarkerController { ui.VoidCallback? onTap, ClusterManagerId? clusterManagerId, GoogleMapsJsBridge? bridge, - }) : _marker = marker, - _infoWindow = infoWindow, - _consumeTapEvents = consumeTapEvents, - _clusterManagerId = clusterManagerId, - tapEvent = onTap, - dragStartEvent = onDragStart, - dragEvent = onDrag, - dragEndEvent = onDragEnd { + }) : _marker = marker, + _infoWindow = infoWindow, + _consumeTapEvents = consumeTapEvents, + _clusterManagerId = clusterManagerId, + tapEvent = onTap, + dragStartEvent = onDragStart, + dragEvent = onDrag, + dragEndEvent = onDragEnd { if (bridge != null) { _addMarkerEvent(bridge); } @@ -50,7 +50,8 @@ class MarkerController { LatLngCallback? dragEndEvent; Future _addMarkerEvent(GoogleMapsJsBridge bridge) async { - final String command = ''' + 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}))); diff --git a/packages/google_maps_flutter/lib/src/marker_clustering.dart b/packages/google_maps_flutter/lib/src/marker_clustering.dart index dc3271cf9..4e1d4f88d 100644 --- a/packages/google_maps_flutter/lib/src/marker_clustering.dart +++ b/packages/google_maps_flutter/lib/src/marker_clustering.dart @@ -18,11 +18,11 @@ class ClusterManagersController extends GeometryController { ClusterManagersController({ required StreamController> stream, required GoogleMapsJsBridge bridge, - }) : _streamController = stream, - _bridge = bridge, - _idToClusterManagerId = {}, - _clusterManagerIdToMarkerClusterer = - {}; + }) : _streamController = stream, + _bridge = bridge, + _idToClusterManagerId = {}, + _clusterManagerIdToMarkerClusterer = + {}; // The stream over which cluster managers broadcast their events final StreamController> _streamController; @@ -31,7 +31,7 @@ class ClusterManagersController extends GeometryController { // 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]. diff --git a/packages/google_maps_flutter/lib/src/markers.dart b/packages/google_maps_flutter/lib/src/markers.dart index 6595c962a..e09dbe2fd 100644 --- a/packages/google_maps_flutter/lib/src/markers.dart +++ b/packages/google_maps_flutter/lib/src/markers.dart @@ -12,11 +12,11 @@ class MarkersController extends GeometryController { required StreamController> stream, required ClusterManagersController clusterManagersController, required GoogleMapsJsBridge bridge, - }) : _streamController = stream, - _clusterManagersController = clusterManagersController, - _bridge = bridge, - _idToMarkerId = {}, - _markerIdToController = {}; + }) : _streamController = stream, + _clusterManagersController = clusterManagersController, + _bridge = bridge, + _idToMarkerId = {}, + _markerIdToController = {}; // A cache of [MarkerController]s indexed by their [MarkerId]. final Map _markerIdToController; @@ -178,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 0997f976b..82b43a0e7 100644 --- a/packages/google_maps_flutter/lib/src/polygon.dart +++ b/packages/google_maps_flutter/lib/src/polygon.dart @@ -13,9 +13,9 @@ class PolygonController { bool consumeTapEvents = false, ui.VoidCallback? onTap, required GoogleMapsJsBridge bridge, - }) : _polygon = polygon, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { + }) : _polygon = polygon, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { _addPolygonEvent(bridge); } diff --git a/packages/google_maps_flutter/lib/src/polygons.dart b/packages/google_maps_flutter/lib/src/polygons.dart index 4fd1f2222..5690f53c4 100644 --- a/packages/google_maps_flutter/lib/src/polygons.dart +++ b/packages/google_maps_flutter/lib/src/polygons.dart @@ -11,10 +11,10 @@ class PolygonsController extends GeometryController { PolygonsController({ required StreamController> stream, required GoogleMapsJsBridge bridge, - }) : _streamController = stream, - _bridge = bridge, - _polygonIdToController = {}, - _idToPolygonId = {}; + }) : _streamController = stream, + _bridge = bridge, + _polygonIdToController = {}, + _idToPolygonId = {}; // A cache of [PolygonController]s indexed by their [PolygonId]. final Map _polygonIdToController; diff --git a/packages/google_maps_flutter/lib/src/polyline.dart b/packages/google_maps_flutter/lib/src/polyline.dart index cb798d0a6..489a99fb1 100644 --- a/packages/google_maps_flutter/lib/src/polyline.dart +++ b/packages/google_maps_flutter/lib/src/polyline.dart @@ -13,9 +13,9 @@ class PolylineController { bool consumeTapEvents = false, ui.VoidCallback? onTap, required GoogleMapsJsBridge bridge, - }) : _polyline = polyline, - _consumeTapEvents = consumeTapEvents, - tapEvent = onTap { + }) : _polyline = polyline, + _consumeTapEvents = consumeTapEvents, + tapEvent = onTap { _addPolylineEvent(bridge); } diff --git a/packages/google_maps_flutter/lib/src/polylines.dart b/packages/google_maps_flutter/lib/src/polylines.dart index 94973d36a..6d9cfe3b0 100644 --- a/packages/google_maps_flutter/lib/src/polylines.dart +++ b/packages/google_maps_flutter/lib/src/polylines.dart @@ -11,10 +11,10 @@ class PolylinesController extends GeometryController { PolylinesController({ required StreamController> stream, required GoogleMapsJsBridge bridge, - }) : _streamController = stream, - _bridge = bridge, - _polylineIdToController = {}, - _idToPolylineId = {}; + }) : _streamController = stream, + _bridge = bridge, + _polylineIdToController = {}, + _idToPolylineId = {}; // A cache of [PolylineController]s indexed by their [PolylineId]. final Map _polylineIdToController; diff --git a/packages/google_maps_flutter/lib/src/util.dart b/packages/google_maps_flutter/lib/src/util.dart index 57cd3c4e4..daff8d4c2 100644 --- a/packages/google_maps_flutter/lib/src/util.dart +++ b/packages/google_maps_flutter/lib/src/util.dart @@ -153,18 +153,15 @@ class GInfoWindowOptions { class GInfoWindow { /// GInfoWindow Constructor. GInfoWindow(GoogleMapsJsBridge bridge, GInfoWindowOptions? opts) - : _bridge = bridge, - _id = _gid++ { + : _bridge = bridge, + _id = _gid++ { _createInfoWindow(opts); } final GoogleMapsJsBridge _bridge; Future _createInfoWindow(GInfoWindowOptions? opts) async { - await _bridge.createObject( - toString(), - 'new google.maps.InfoWindow($opts)', - ); + await _bridge.createObject(toString(), 'new google.maps.InfoWindow($opts)'); } final int _id; @@ -218,9 +215,9 @@ class GInfoWindow { class GMarker { /// GMarker Constructor. GMarker(GoogleMapsJsBridge bridge, [GMarkerOptions? opts]) - : _bridge = bridge, - id = _gid++, - _options = opts { + : _bridge = bridge, + id = _gid++, + _options = opts { _createMarker(opts); } @@ -301,9 +298,7 @@ class GMarker { } Future _setVisible(bool? visible) async { - await _bridge.callMethod(JsRef(toString()), 'setVisible', [ - visible, - ]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setDraggable(bool? visible) async { @@ -317,9 +312,7 @@ class GMarker { } Future _setOpacity(num? opacity) async { - await _bridge.callMethod(JsRef(toString()), 'setOpacity', [ - opacity, - ]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [opacity]); } Future _setPosition(LatLng? position) async { @@ -344,18 +337,15 @@ class GMarker { class GPolyline { /// GPolyline Constructor. GPolyline(GoogleMapsJsBridge bridge, [GPolylineOptions? opts]) - : _bridge = bridge, - id = _gid++ { + : _bridge = bridge, + id = _gid++ { _createPolyline(opts); } final GoogleMapsJsBridge _bridge; Future _createPolyline(GPolylineOptions? opts) async { - await _bridge.createObject( - toString(), - 'new google.maps.Polyline($opts)', - ); + await _bridge.createObject(toString(), 'new google.maps.Polyline($opts)'); } /// GPolyline id. @@ -379,9 +369,7 @@ class GPolyline { } Future _setVisible(bool? visible) async { - await _bridge.callMethod(JsRef(toString()), 'setVisible', [ - visible, - ]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { @@ -446,8 +434,8 @@ class GPolylineOptions { class GPolygon { /// GPolygon Constructor. GPolygon(GoogleMapsJsBridge bridge, [GPolygonOptions? opts]) - : _bridge = bridge, - id = _gid++ { + : _bridge = bridge, + id = _gid++ { _createPolygon(opts); } @@ -478,9 +466,7 @@ class GPolygon { } Future _setVisible(bool? visible) async { - await _bridge.callMethod(JsRef(toString()), 'setVisible', [ - visible, - ]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setMap(Object? /*GMap?|StreetViewPanorama?*/ map) async { @@ -555,8 +541,8 @@ class GPolygonOptions { class GCircle { /// GCircle Constructor. GCircle(GoogleMapsJsBridge bridge, [GCircleOptions? opts]) - : _bridge = bridge, - id = _gid++ { + : _bridge = bridge, + id = _gid++ { _createCircle(opts); } @@ -590,9 +576,7 @@ class GCircle { } Future _setVisible(bool? visible) async { - await _bridge.callMethod(JsRef(toString()), 'setVisible', [ - visible, - ]); + await _bridge.callMethod(JsRef(toString()), 'setVisible', [visible]); } Future _setRadius(num? radius) async { @@ -605,11 +589,9 @@ class GCircle { } Future _setOptions(GCircleOptions? options) async { - await _bridge.callMethod( - JsRef(toString()), - 'setOptions', - [options], - ); + await _bridge.callMethod(JsRef(toString()), 'setOptions', [ + options, + ]); } } @@ -657,8 +639,8 @@ class GCircleOptions { class GMarkerClusterer { /// GMarkerCluster Constructor. GMarkerClusterer(GoogleMapsJsBridge bridge, [GMarkerClustererOptions? opts]) - : _bridge = bridge, - id = _gid++ { + : _bridge = bridge, + id = _gid++ { _createMarkerClusterer(opts); } @@ -681,9 +663,11 @@ class GMarkerClusterer { /// Removes a marker from the [GMarkerClusterer]. Future removeMarker(GMarker marker, bool? noDraw) async { - final bool result = await _bridge.runJavaScriptReturningResult( - '${toString()}.removeMarker($marker, $noDraw);', - ) as bool; + final bool result = + await _bridge.runJavaScriptReturningResult( + '${toString()}.removeMarker($marker, $noDraw);', + ) + as bool; return result; } @@ -775,9 +759,9 @@ class GMarkerClustererOptions { class GGroundOverlay { /// GGroundOverlay Constructor. GGroundOverlay(GoogleMapsJsBridge bridge, [GGroundOverlayOptions? opts]) - : _bridge = bridge, - id = _gid++, - _options = opts { + : _bridge = bridge, + id = _gid++, + _options = opts { _createGroundOverlay(opts); } @@ -835,9 +819,7 @@ class GGroundOverlay { } Future _setOpacity(num? opacity) async { - await _bridge.callMethod(JsRef(toString()), 'setOpacity', [ - opacity, - ]); + await _bridge.callMethod(JsRef(toString()), 'setOpacity', [opacity]); } } From dcaba3c3b333867f9ef588a4cfa5336eface850f Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Mon, 10 Aug 2026 18:46:08 +0900 Subject: [PATCH 10/12] [google_maps_flutter] Stop double-quoting info window HTML content _infoWindowOptionsFromMarker wrapped content in manual JS string quotes, but its consumers (GInfoWindowOptions.toString and GInfoWindow._setContent) already JSON-encode the value, causing literal apostrophes to render in the InfoWindow. --- packages/google_maps_flutter/CHANGELOG.md | 3 +++ packages/google_maps_flutter/lib/src/convert.dart | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index c1d92c925..95519f495 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -5,6 +5,9 @@ * Update `webview_flutter_lwe` to ^0.5.3. * Raise the SDK constraint to match `webview_flutter_lwe` 0.5.3 (Dart ^3.8.0, Flutter >=3.32.0). +* Fix info window content being rendered with stray quote characters. +* Cancel the bridge event subscription on dispose to avoid a late event + throwing a `StateError` on an already-closed stream. * Verify integration tests pass against upstream google_maps_flutter v2.17.0. ## 0.1.14 diff --git a/packages/google_maps_flutter/lib/src/convert.dart b/packages/google_maps_flutter/lib/src/convert.dart index 03d5c1ef1..82388f4e8 100644 --- a/packages/google_maps_flutter/lib/src/convert.dart +++ b/packages/google_maps_flutter/lib/src/convert.dart @@ -307,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); @@ -320,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() From c6dfa15fc303bd6469e79665a6e5f7b0040890db Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Mon, 10 Aug 2026 18:46:14 +0900 Subject: [PATCH 11/12] [google_maps_flutter] Cancel the bridge event subscription on dispose The subscription to _bridge.events was never retained, so a JS event queued just before dispose() could still invoke _onJsEvent afterward. Handlers like _onIdle() add to _streamController unguarded, which throws a StateError once the controller is closed. --- .../google_maps_flutter/lib/src/google_maps_controller.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 931e3583b..45bcdd0e5 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_controller.dart @@ -140,8 +140,10 @@ class GoogleMapsController { return _isTrafficLayerEnabled(_rawMapOptions); } + StreamSubscription? _bridgeEventsSubscription; + void _getWebview() { - _bridge.events.listen(_onJsEvent); + _bridgeEventsSubscription = _bridge.events.listen(_onJsEvent); _webview = WebViewWidget(controller: _bridge.controller); } @@ -869,6 +871,7 @@ 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; From 1197f4ef970e9e0e9171b380964f699f50270d30 Mon Sep 17 00:00:00 2001 From: Seungsoo Lee Date: Tue, 11 Aug 2026 16:37:26 +0900 Subject: [PATCH 12/12] [google_maps_flutter] Simplify JS event model and fix PR review nits Replace the sealed-class MapsJsEvent hierarchy and the GoogleMapsJsBridge/WebViewGoogleMapsJsBridge interface split with a single enum+record event type and one concrete bridge class, since there was only ever one implementation. Also fixes the copyright header year on the newly added bridge file and bumps the package to 0.2.0 since removing GoogleMapsController.controller is a breaking change. --- packages/google_maps_flutter/CHANGELOG.md | 6 +- packages/google_maps_flutter/README.md | 2 +- .../lib/src/google_maps_controller.dart | 54 +-- .../lib/src/google_maps_js_bridge.dart | 349 ++++++------------ packages/google_maps_flutter/pubspec.yaml | 2 +- 5 files changed, 139 insertions(+), 274 deletions(-) diff --git a/packages/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/CHANGELOG.md index 95519f495..a64d776e1 100644 --- a/packages/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/CHANGELOG.md @@ -1,7 +1,9 @@ -## 0.1.15 +## 0.2.0 * Refactor the JavaScript interaction layer by introducing `GoogleMapsJsBridge`, - replacing direct `WebViewController` calls. + replacing direct `WebViewController` calls. This removes the public + `GoogleMapsController.controller` field; use the plugin's public API + instead of reaching into the WebView controller directly. * Update `webview_flutter_lwe` to ^0.5.3. * Raise the SDK constraint to match `webview_flutter_lwe` 0.5.3 (Dart ^3.8.0, Flutter >=3.32.0). diff --git a/packages/google_maps_flutter/README.md b/packages/google_maps_flutter/README.md index 285e729a5..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.15 + 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/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/lib/src/google_maps_controller.dart index 45bcdd0e5..9fd427800 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_controller.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_controller.dart @@ -77,7 +77,7 @@ class GoogleMapsController { /// The bridge mediating all interaction with the Google Maps JavaScript /// API running inside the WebView. - final GoogleMapsJsBridge _bridge = WebViewGoogleMapsJsBridge(); + final GoogleMapsJsBridge _bridge = GoogleMapsJsBridge(); /// The Flutter widget that will contain the rendered Map. Used for caching. WebViewWidget? get webview => _webview; @@ -148,35 +148,35 @@ class GoogleMapsController { } Future _onJsEvent(MapsJsEvent event) async { - switch (event) { - case BoundsChangedJsEvent(): + switch (event.type) { + case MapsJsEventType.boundsChanged: await _onBoundsChanged(); - case IdleJsEvent(): + case MapsJsEventType.idle: _onIdle(); - case TilesLoadedJsEvent(): + case MapsJsEventType.tilesLoaded: _onTilesloaded(); - case ClickJsEvent(message: final String message): - _onClick(message); - case LongPressJsEvent(message: final String message): - _onLongPress(message); - case MarkerClickJsEvent(message: final String message): - _onMarkerClick(message); - case ClusterClickJsEvent(message: final String message): - _onClusterClick(message); - case MarkerDragStartJsEvent(message: final String message): - _onMarkerDragStart(message); - case MarkerDragJsEvent(message: final String message): - _onMarkerDrag(message); - case MarkerDragEndJsEvent(message: final String message): - _onMarkerDragEnd(message); - case PolylineClickJsEvent(message: final String message): - _onPolylineClick(message); - case PolygonClickJsEvent(message: final String message): - _onPolygonClick(message); - case CircleClickJsEvent(message: final String message): - _onCircleClick(message); - case GroundOverlayClickJsEvent(message: final String message): - _onGroundOverlayClick(message); + 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!); } } 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 index 738fd5e40..ad8bb4149 100644 --- a/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart +++ b/packages/google_maps_flutter/lib/src/google_maps_js_bridge.dart @@ -1,4 +1,4 @@ -// Copyright 2021 Samsung Electronics Co., Ltd. All rights reserved. +// 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. @@ -42,105 +42,58 @@ class JsExpression { String toString() => code; } -/// Base class for events dispatched from the Google Maps JavaScript runtime -/// back into Dart through a [GoogleMapsJsBridge]. -sealed class MapsJsEvent { - /// Const constructor for subclasses. - const MapsJsEvent(); -} +/// Identifies the kind of a [MapsJsEvent] dispatched from the Google Maps +/// JavaScript runtime. +enum MapsJsEventType { + /// The map's `bounds_changed` listener fired. + boundsChanged, -/// Fired when the map's `bounds_changed` listener fires. -class BoundsChangedJsEvent extends MapsJsEvent { - /// Creates a [BoundsChangedJsEvent]. - const BoundsChangedJsEvent(); -} + /// The map's `idle` listener fired. + idle, -/// Fired when the map's `idle` listener fires. -class IdleJsEvent extends MapsJsEvent { - /// Creates an [IdleJsEvent]. - const IdleJsEvent(); -} + /// The map's `tilesloaded` listener fired. + tilesLoaded, -/// Fired when the map's `tilesloaded` listener fires. -class TilesLoadedJsEvent extends MapsJsEvent { - /// Creates a [TilesLoadedJsEvent]. - const TilesLoadedJsEvent(); -} + /// The map was clicked. + click, -/// Base class for [MapsJsEvent]s that carry a raw JSON [message] payload. -sealed class MessageJsEvent extends MapsJsEvent { - /// Creates a [MessageJsEvent] carrying the raw [message] payload. - const MessageJsEvent(this.message); + /// The map was long-pressed. + longPress, - /// The raw (JSON-encoded) message payload posted from the JS side. - final String message; -} - -/// Fired when the map is clicked. -class ClickJsEvent extends MessageJsEvent { - /// Creates a [ClickJsEvent]. - const ClickJsEvent(super.message); -} - -/// Fired when the map is long-pressed. -class LongPressJsEvent extends MessageJsEvent { - /// Creates a [LongPressJsEvent]. - const LongPressJsEvent(super.message); -} + /// A marker was clicked. + markerClick, -/// Fired when a marker is clicked. -class MarkerClickJsEvent extends MessageJsEvent { - /// Creates a [MarkerClickJsEvent]. - const MarkerClickJsEvent(super.message); -} + /// A marker cluster was clicked. + clusterClick, -/// Fired when a marker cluster is clicked. -class ClusterClickJsEvent extends MessageJsEvent { - /// Creates a [ClusterClickJsEvent]. - const ClusterClickJsEvent(super.message); -} + /// A marker drag started. + markerDragStart, -/// Fired when a marker drag starts. -class MarkerDragStartJsEvent extends MessageJsEvent { - /// Creates a [MarkerDragStartJsEvent]. - const MarkerDragStartJsEvent(super.message); -} + /// A marker is being dragged. + markerDrag, -/// Fired while a marker is being dragged. -class MarkerDragJsEvent extends MessageJsEvent { - /// Creates a [MarkerDragJsEvent]. - const MarkerDragJsEvent(super.message); -} + /// A marker drag ended. + markerDragEnd, -/// Fired when a marker drag ends. -class MarkerDragEndJsEvent extends MessageJsEvent { - /// Creates a [MarkerDragEndJsEvent]. - const MarkerDragEndJsEvent(super.message); -} + /// A polyline was clicked. + polylineClick, -/// Fired when a polyline is clicked. -class PolylineClickJsEvent extends MessageJsEvent { - /// Creates a [PolylineClickJsEvent]. - const PolylineClickJsEvent(super.message); -} + /// A polygon was clicked. + polygonClick, -/// Fired when a polygon is clicked. -class PolygonClickJsEvent extends MessageJsEvent { - /// Creates a [PolygonClickJsEvent]. - const PolygonClickJsEvent(super.message); -} + /// A circle was clicked. + circleClick, -/// Fired when a circle is clicked. -class CircleClickJsEvent extends MessageJsEvent { - /// Creates a [CircleClickJsEvent]. - const CircleClickJsEvent(super.message); + /// A ground overlay was clicked. + groundOverlayClick, } -/// Fired when a ground overlay is clicked. -class GroundOverlayClickJsEvent extends MessageJsEvent { - /// Creates a [GroundOverlayClickJsEvent]. - const GroundOverlayClickJsEvent(super.message); -} +/// 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. @@ -148,79 +101,49 @@ class GroundOverlayClickJsEvent extends MessageJsEvent { /// 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. -abstract class GoogleMapsJsBridge { - /// The underlying WebView controller. Exposed so callers can build the - /// [WebViewWidget] that hosts this bridge's JS runtime. - WebViewController get controller; - - /// Broadcasts events received from the JS side. - Stream get events; - - /// Loads the map HTML shell and wires up the JS→Dart event channels. - /// - /// Completes once the page has finished loading. - Future load(); - - /// Creates the top-level `map` JS variable using [optionsJs] (a JS object - /// literal), plus its built-in map-level listeners. - Future createMap(String optionsJs); - - /// 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); - - /// Assigns `ref[property] = value` on the JS side. - Future setProperty(JsRef ref, String property, Object? value); - - /// Reads `ref.property` from the JS side. - Future getProperty(JsRef ref, String property); - - /// Calls `ref.method(...args)` on the JS side, discarding the result. - Future callMethod(JsRef ref, String method, List args); - - /// Calls `ref.method(...args)` on the JS side and returns the result. - Future callMethodReturning( - JsRef ref, - String method, - List args, - ); - - /// 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, - ); - - /// Escape hatch for JS not yet expressed in terms of the methods above. - Future runJavaScript(String script); - - /// Escape hatch for JS not yet expressed in terms of the methods above, - /// returning the raw result. - Future runJavaScriptReturningResult(String script); - - /// Releases the resources held by this bridge. - void dispose(); -} - -/// A [GoogleMapsJsBridge] backed by a [WebViewController]. -class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { +class GoogleMapsJsBridge { /// Creates a bridge over [controller], or a fresh [WebViewController] if /// none is provided. - WebViewGoogleMapsJsBridge({WebViewController? controller}) + GoogleMapsJsBridge({WebViewController? controller}) : controller = controller ?? WebViewController(); - @override + /// The JS-side channel name and payload-less-ness 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(); - @override + /// Broadcasts events received from the JS side. Stream get events => _events.stream; /// Adds [event] to [_events], unless this bridge has already been @@ -235,7 +158,9 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { } } - @override + /// Loads the map HTML shell and wires up the JS→Dart event channels. + /// + /// Completes once the page has finished loading. Future load() { String path = Platform.environment['AUL_ROOT_PATH'] ?? ''; path += '/res/flutter_assets/assets/map.html'; @@ -249,97 +174,31 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { }, ), ) - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..addJavaScriptChannel( - 'BoundChanged', - onMessageReceived: (JavaScriptMessage message) { - _emit(const BoundsChangedJsEvent()); - }, - ) - ..addJavaScriptChannel( - 'Idle', - onMessageReceived: (JavaScriptMessage message) { - _emit(const IdleJsEvent()); - }, - ) - ..addJavaScriptChannel( - 'Tilesloaded', - onMessageReceived: (JavaScriptMessage message) { - _emit(const TilesLoadedJsEvent()); - }, - ) - ..addJavaScriptChannel( - 'Click', - onMessageReceived: (JavaScriptMessage message) { - _emit(ClickJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'LongPress', - onMessageReceived: (JavaScriptMessage message) { - _emit(LongPressJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'MarkerClick', - onMessageReceived: (JavaScriptMessage message) { - _emit(MarkerClickJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'ClusterClick', - onMessageReceived: (JavaScriptMessage message) { - _emit(ClusterClickJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'MarkerDragStart', - onMessageReceived: (JavaScriptMessage message) { - _emit(MarkerDragStartJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'MarkerDrag', - onMessageReceived: (JavaScriptMessage message) { - _emit(MarkerDragJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'MarkerDragEnd', - onMessageReceived: (JavaScriptMessage message) { - _emit(MarkerDragEndJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'PolylineClick', - onMessageReceived: (JavaScriptMessage message) { - _emit(PolylineClickJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'PolygonClick', - onMessageReceived: (JavaScriptMessage message) { - _emit(PolygonClickJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'CircleClick', - onMessageReceived: (JavaScriptMessage message) { - _emit(CircleClickJsEvent(message.message)); - }, - ) - ..addJavaScriptChannel( - 'GroundOverlayClick', + ..setJavaScriptMode(JavaScriptMode.unrestricted); + + for (final MapEntry entry + in _channelEventTypes.entries) { + final MapsJsEventType type = entry.value; + controller.addJavaScriptChannel( + entry.key, onMessageReceived: (JavaScriptMessage message) { - _emit(GroundOverlayClickJsEvent(message.message)); + _emit(( + type: type, + message: _payloadlessEventTypes.contains(type) + ? null + : message.message, + )); }, - ) - ..loadFile(path); + ); + } + + controller.loadFile(path); return _pageFinished.future; } - @override + /// 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 = ''' @@ -378,7 +237,8 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { await controller.runJavaScript(command); } - @override + /// 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, @@ -403,19 +263,19 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { return arg.toString(); } - @override + /// 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)})", ); } - @override + /// Reads `ref.property` from the JS side. Future getProperty(JsRef ref, String property) async { return controller.runJavaScriptReturningResult('$ref.$property'); } - @override + /// 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( @@ -423,7 +283,7 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { ); } - @override + /// Calls `ref.method(...args)` on the JS side and returns the result. Future callMethodReturning( JsRef ref, String method, @@ -435,7 +295,9 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { ); } - @override + /// 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, @@ -447,17 +309,18 @@ class WebViewGoogleMapsJsBridge implements GoogleMapsJsBridge { ); } - @override + /// Escape hatch for JS not yet expressed in terms of the methods above. Future runJavaScript(String script) async { await controller.runJavaScript(script); } - @override + /// 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); } - @override + /// Releases the resources held by this bridge. void dispose() { _events.close(); } diff --git a/packages/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/pubspec.yaml index 6b4d675bb..6a04dd2a6 100644 --- a/packages/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ 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.15 +version: 0.2.0 environment: sdk: ^3.8.0