Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## NEXT
## 0.5.15

* Updates minimum supported SDK version to Flutter 3.35/Dart 3.9.
* Batches clustered marker add/remove operations to avoid redundant re-rendering.

## 0.5.14+3

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'package:google_maps_flutter_web/src/google_maps_inspector_web.dart';
import 'package:google_maps_flutter_web/src/marker_clustering.dart';
import 'package:integration_test/integration_test.dart';

void main() {
Expand All @@ -23,9 +25,14 @@ void main() {
const initialCameraPosition = CameraPosition(target: mapCenter);

group('MarkersController', () {
const testMapId = 33930;
late int testMapId;

tearDown(() {
plugin.dispose(mapId: testMapId);
});

testWidgets('Marker clustering', (WidgetTester tester) async {
testMapId = 33930;
const clusterManagerId = ClusterManagerId('cluster 1');

final clusterManagers = <ClusterManager>{
Expand Down Expand Up @@ -67,8 +74,6 @@ void main() {
final int mapId = await mapIdCompleter.future;
expect(mapId, equals(testMapId));

addTearDown(() => plugin.dispose(mapId: mapId));

final List<Cluster> clusters =
await waitForValueMatchingPredicate<List<Cluster>>(
tester,
Expand Down Expand Up @@ -105,6 +110,83 @@ void main() {

expect(updatedClusters.length, 0);
});

testWidgets('clusters render once per batched add', (
WidgetTester tester,
) async {
const clusterManagerId = ClusterManagerId('cluster 1');

final clusterManagers = <ClusterManager>{
const ClusterManager(clusterManagerId: clusterManagerId),
};

// Create the marker with clusterManagerId.
final initialMarkers = <Marker>{
for (var i = 0; i < 3; i++)
Marker(
markerId: MarkerId(i.toString()),
position: mapCenter,
clusterManagerId: clusterManagerId,
),
};

final markersCluster1 = <Marker>{
for (var i = 3; i < 7; i++)
Marker(
markerId: MarkerId(i.toString()),
clusterManagerId: clusterManagerId,
position: mapCenter,
),
};

testMapId = 33931;
final events = StreamController<ClusteringEvent>();
await _pumpMap(
tester,
plugin.buildViewWithConfiguration(
testMapId,
(int id) async {
final StreamSubscription<ClusteringEvent>? subscription =
(inspector as GoogleMapsInspectorWeb)
.getClusteringEvents(
mapId: testMapId,
clusterManagerId: clusterManagerId,
)
?.listen(events.add);

await plugin.updateMarkers(
MarkerUpdates.from(initialMarkers, markersCluster1),
mapId: testMapId,
);

await Future<void>.delayed(const Duration(seconds: 1));
await subscription?.cancel();
await events.close();
},
widgetConfiguration: const MapWidgetConfiguration(
initialCameraPosition: initialCameraPosition,
textDirection: TextDirection.ltr,
),
mapObjects: MapObjects(
clusterManagers: clusterManagers,
markers: initialMarkers,
),
),
);

await expectLater(
events.stream,
emitsInAnyOrder([
//Once per initial markers
ClusteringEvent.begin,
ClusteringEvent.end,
//Once per new cluster
ClusteringEvent.begin,
ClusteringEvent.end,
emitsDone,
]),
);
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// found in the LICENSE file.

import 'dart:js_interop';
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:google_maps/google_maps.dart' as gmaps;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';

Expand Down Expand Up @@ -150,4 +150,15 @@ class GoogleMapsInspectorWeb extends GoogleMapsInspectorPlatform {
)?.getClusters(clusterManagerId) ??
<Cluster>[];
}

/// Returns the stream of clustering events for a given [ClusterManager].
@visibleForTesting
Stream<ClusteringEvent>? getClusteringEvents({
required int mapId,
required ClusterManagerId clusterManagerId,
}) {
return _clusterManagersControllerProvider(
mapId,
)?.getClustererEvents(clusterManagerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf

import '../google_maps_flutter_web.dart';
import 'marker_clustering_js_interop.dart';
import 'marker_clustering_js_interop.dart' as interop;
import 'types.dart';

/// Events emitted by the marker clustering lifecycle.
enum ClusteringEvent {
/// Clustering has started.
begin,

/// Clustering finished and clusters are available.
end,
}

/// A controller class for managing marker clustering.
///
/// This class maps [ClusterManager] objects to javascript [MarkerClusterer]
Expand Down Expand Up @@ -82,6 +92,17 @@ class ClusterManagersController extends GeometryController {
}
}

/// Adds given list of [gmaps.Marker] to the [MarkerClusterer] with given
/// [ClusterManagerId].
void addItems(ClusterManagerId clusterManagerId, List<gmaps.Marker> markers) {
final MarkerClusterer? markerClusterer =
_clusterManagerIdToMarkerClusterer[clusterManagerId];
if (markerClusterer != null) {
markerClusterer.addMarkers(markers, true);
markerClusterer.render();
}
}

/// Removes given [gmaps.Marker] from the [MarkerClusterer] with given
/// [ClusterManagerId].
void removeItem(ClusterManagerId clusterManagerId, gmaps.Marker? marker) {
Expand All @@ -95,6 +116,22 @@ class ClusterManagersController extends GeometryController {
}
}

/// Removes given [gmaps.Marker] from the [MarkerClusterer] with given
/// [ClusterManagerId].
void removeItems(
ClusterManagerId clusterManagerId,
List<gmaps.Marker>? markers,
) {
if (markers != null) {
final MarkerClusterer? markerClusterer =
_clusterManagerIdToMarkerClusterer[clusterManagerId];
if (markerClusterer != null) {
markerClusterer.removeMarkers(markers, true);
markerClusterer.render();
}
}
}

/// Returns list of clusters in [MarkerClusterer] with given
/// [ClusterManagerId].
List<Cluster> getClusters(ClusterManagerId clusterManagerId) {
Expand All @@ -111,6 +148,13 @@ class ClusterManagersController extends GeometryController {
return <Cluster>[];
}

/// Returns the stream of clustering lifecycle events for the given manager.
Stream<ClusteringEvent>? getClustererEvents(
ClusterManagerId clusterManagerId,
) => interop.getClustererEvents(
_clusterManagerIdToMarkerClusterer[clusterManagerId]!,
);

void _clusterClicked(
ClusterManagerId clusterManagerId,
gmaps.MapMouseEvent event,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
@JS()
library;

import 'dart:async';
import 'dart:js_interop';

import 'package:google_maps/google_maps.dart' as gmaps;

import 'marker_clustering.dart';

/// A typedef representing a callback function for handling cluster tap events.
typedef ClusterClickHandler =
void Function(gmaps.MapMouseEvent, MarkerClustererCluster, gmaps.Map);
Expand Down Expand Up @@ -64,6 +67,16 @@ extension type MarkerClustererOptions._(JSObject _) implements JSObject {
external JSExportedDartFunction? get _onClusterClick;
}

@JS('google.maps.event.addListener')
external JSAny _gmapsAddListener(
JSAny instance,
String eventName,
JSFunction handler,
);

@JS('google.maps.event.removeListener')
external void _gmapsRemoveListener(JSAny listenerHandle);

/// The cluster object handled by the [MarkerClusterer].
///
/// https://googlemaps.github.io/js-markerclusterer/classes/Cluster.html
Expand Down Expand Up @@ -117,7 +130,7 @@ extension type MarkerClusterer._(JSObject _) implements JSObject {

/// Adds a list of markers to be clustered by the [MarkerClusterer].
void addMarkers(List<gmaps.Marker>? markers, bool? noDraw) =>
_addMarkers(markers?.cast<JSAny>().toJS, noDraw);
_addMarkers(markers?.toJS, noDraw);
@JS('addMarkers')
external void _addMarkers(JSArray<JSAny>? markers, bool? noDraw);

Expand All @@ -129,7 +142,7 @@ extension type MarkerClusterer._(JSObject _) implements JSObject {

/// Removes a list of markers from the [MarkerClusterer].
bool removeMarkers(List<gmaps.Marker>? markers, bool? noDraw) =>
_removeMarkers(markers?.cast<JSAny>().toJS, noDraw);
_removeMarkers(markers?.toJS, noDraw);
@JS('removeMarkers')
external bool _removeMarkers(JSArray<JSAny>? markers, bool? noDraw);

Expand Down Expand Up @@ -164,3 +177,29 @@ MarkerClusterer createMarkerClusterer(
);
return MarkerClusterer(options);
}

///Converts events that clustering manager emits during rendering to stream
Stream<ClusteringEvent> getClustererEvents(MarkerClusterer clusterer) {
late final StreamController<ClusteringEvent> controller;

final JSAny beginHandle = _gmapsAddListener(
clusterer,
'clusteringbegin',
((JSAny mc) => controller.add(ClusteringEvent.begin)).toJS,
);

final JSAny endHandle = _gmapsAddListener(
clusterer,
'clusteringend',
((JSAny mc) => controller.add(ClusteringEvent.end)).toJS,
);

controller = StreamController<ClusteringEvent>(
onCancel: () {
_gmapsRemoveListener(beginHandle);
_gmapsRemoveListener(endHandle);
},
);

return controller.stream;
}
Loading