Skip to content

Add clusteringIdentifier for native MKMapView clustering#88

Open
klandri wants to merge 2 commits into
fluttercommunity:masterfrom
klandri:clustering-identifier
Open

Add clusteringIdentifier for native MKMapView clustering#88
klandri wants to merge 2 commits into
fluttercommunity:masterfrom
klandri:clustering-identifier

Conversation

@klandri
Copy link
Copy Markdown

@klandri klandri commented May 19, 2026

Exposes MKAnnotationView.clusteringIdentifier so apps using this plugin can opt into MapKit's native MKClusterAnnotation aggregation. Setting all annotations once and letting MapKit cluster them in C/Obj-C avoids the platform-channel chatter caused by Dart-side clustering on zoom.

Dart:

  • Annotation: nullable clusteringIdentifier field, plumbed through constructor, copyWith, _toJson, and ==.

iOS:

  • FlutterAnnotation: parses clusteringIdentifier from the platform-channel dict; included in ==.
  • AnnotationController.getAnnotationView: sets clusteringIdentifier on the dequeued annotation view (iOS 11+).
  • AnnotationController.viewFor: returns a FlutterClusterAnnotationView for MKClusterAnnotation instances.
  • AnnotationController.didSelect: intercepts taps on cluster annotations and animates the camera to the cluster's member bounding box.
  • FlutterClusterAnnotationView: red circle with the member count drawn in white, sized to digit count, white border.

This results in a massive increase in clustering performance. My app which will ship with this version of the library went from being unusable due to stutters with 5k+ markers to being nice and smooth like a native app.

Full disclosure the code here is fully generated by Claude but tested and working, including on a real device.

Pre-launch Checklist

  • I updated pubspec.yaml with an appropriate new version according to the [pub versioning philosophy].
  • I updated CHANGELOG.md to add a description of the change.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making if a test is possible.
  • All existing and new tests are passing.

klandri added 2 commits May 19, 2026 23:03
Exposes MKAnnotationView.clusteringIdentifier so apps using this plugin
can opt into MapKit's native MKClusterAnnotation aggregation. Setting all
annotations once and letting MapKit cluster them in C/Obj-C avoids the
platform-channel chatter caused by Dart-side clustering on zoom.

Dart:
- Annotation: nullable clusteringIdentifier field, plumbed through
  constructor, copyWith, _toJson, and ==.

iOS:
- FlutterAnnotation: parses clusteringIdentifier from the platform-channel
  dict; included in ==.
- AnnotationController.getAnnotationView: sets clusteringIdentifier on
  the dequeued annotation view (iOS 11+).
- AnnotationController.viewFor: returns a FlutterClusterAnnotationView
  for MKClusterAnnotation instances.
- AnnotationController.didSelect: intercepts taps on cluster annotations
  and animates the camera to the cluster's member bounding box.
- FlutterClusterAnnotationView: red circle with the member count drawn
  in white, sized to digit count, white border.
…n diff

Two independent perf fixes that compound for large annotation sets (5k+).

iOS:
- Maintain a [String: FlutterAnnotation] dict on AppleMapController, kept
  in sync with mapView.annotations by every mutator. getAnnotation,
  annotationExists, removeAnnotation, annotationsToChange, and
  annotationsIdsToRemove now do O(1) lookups instead of O(N) filters,
  dropping their loops from O(N^2) to O(N).
- annotationsIdsToRemove batches into a single
  mapView.removeAnnotations([...]) call instead of N single removes.
- Track maxAnnotationZIndex incrementally so getNextAnnotationZIndex and
  isAnnotationInFront are O(1) instead of an O(N log N) sort of every
  annotation on the map. (zIndex is monotonically increasing; removing
  the max doesn't decrement, which is fine for the next-id use case.)
- isAnnotationSelected stops doing N getAnnotation calls per
  selectedAnnotation; checks the dict once.

Dart:
- _AnnotationUpdates.from now only includes annotations in
  annotationsToChange where current != previous. Previously every
  annotation whose id was in both old and new sets was serialized
  across the platform channel on every diff, even when its content
  was identical. For 5k annotation sets this serialized megabytes of
  unchanged data per update.

Tests:
- Unskip the existing "Partial Update" test, which was written to
  verify exactly this behavior and was skipped because the buggy
  diff failed it.
- Adjust "Adding an annotation" to assert the unchanged annotation
  is NOT in annotationsToChange (it previously asserted the buggy
  behavior).
- Add "Unchanged annotation is not in annotationsToChange" covering
  the case where annotation instances differ but their content is
  identical (e.g., rebuilt by setState).

For an app with 5k annotations, filter operations that previously
hung the UI for multiple seconds now complete in tens of milliseconds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant