mapkit_flutter 0.3.0 copy "mapkit_flutter: ^0.3.0" to clipboard
mapkit_flutter: ^0.3.0 copied to clipboard

PlatformiOSmacOS

MapKit for Flutter. Display MKMapView on iOS with annotations, overlays, clustering, look-around, tile overlays, and modern MapKit configurations.

mapkit_flutter #

pub package

MapKit for Flutter. Wraps MKMapView as a Flutter platform view on iOS and macOS, with annotations, overlays, clustering, Look Around, tile overlays, and the modern MKMapConfiguration family.

Every public type carries Apple's exact MapKit symbol nameMKMapCamera, MKCoordinateRegion, MKPolyline, CLLocationCoordinate2D. If you (or your coding agent) know MapKit, you already know this API. The MK namespace also means zero import collisions with google_maps_flutter, mapbox_maps_flutter, or flutter_map in mixed-platform code — no as mk prefixes needed.

Apple platforms only (iOS + macOS). Apple's MapKit does not exist on Android. Pair this with google_maps_flutter behind a platform switch for cross-platform apps. Look Around is iOS-only (no macOS MapKit equivalent).

Install #

flutter pub add mapkit_flutter

Add to ios/Runner/Info.plist if you set showsUserLocation: true:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to show your current location on the map.</string>

Minimum: iOS 17, macOS 14, Dart 3.10, Flutter 3.41.

Quick start #

import 'package:flutter/material.dart';
import 'package:mapkit_flutter/mapkit_flutter.dart';

class MapPage extends StatefulWidget {
  const MapPage({super.key});

  @override
  State<MapPage> createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
  MKMapViewController? _controller;

  @override
  Widget build(BuildContext context) {
    return MKMapView(
      initialCamera: const MKMapCamera(
        centerCoordinate: CLLocationCoordinate2D(
          latitude: 37.334922,
          longitude: -122.009033,
        ),
        distance: 1500, // meters, like MKMapCamera.centerCoordinateDistance
      ),
      showsUserLocation: true,
      annotations: {
        const MKPointAnnotation(
          id: MKAnnotationId('apple-park'),
          coordinate: CLLocationCoordinate2D(
            latitude: 37.334922,
            longitude: -122.009033,
          ),
          title: 'Apple Park',
          subtitle: 'One Apple Park Way',
        ),
      },
      onMapCreated: (controller) => _controller = controller,
      onTap: (coordinate) => debugPrint('tapped $coordinate'),
    );
  }
}

Coordinate is an exported alias for CLLocationCoordinate2D.

The widget mirrors MKMapView #

The base style is a sealed preferredConfiguration (exactly MKMapView.preferredConfiguration); view-level switches are direct parameters with their MKMapView property names:

MKMapView(
  initialCamera: ...,
  preferredConfiguration: const MKStandardMapConfiguration(
    elevationStyle: MKMapElevationStyle.realistic,
    emphasisStyle: MKMapEmphasisStyle.muted, // `standard` ≙ Apple's `.default`
    pointOfInterestFilter: MKPointOfInterestFilter.including([
      MKPointOfInterestCategory.cafe,
      MKPointOfInterestCategory.park,
    ]),
    showsTraffic: true,
  ),
  // MKHybridMapConfiguration() / MKImageryMapConfiguration() likewise.
  isZoomEnabled: true,
  isScrollEnabled: true,
  isRotateEnabled: false,
  isPitchEnabled: false,
  showsCompass: true,
  showsScale: true,
  showsUserLocation: true,
  showsUserTrackingButton: true,
  userTrackingMode: MKUserTrackingMode.follow,
  cameraZoomRange: const MKCameraZoomRange(
    minCenterCoordinateDistance: 500,    // meters
    maxCenterCoordinateDistance: 100000,
  ),
  cameraBoundary: someRegion,            // MKCoordinateRegion?
  selectableMapFeatures: {MKMapFeatureOptions.pointsOfInterest},
)

Annotations #

MKPointAnnotation merges the annotation (coordinate, title, subtitle) with its MKAnnotationView presentation (alpha, isDraggable, zPriority, clusteringIdentifier…) — Flutter is declarative, so there is no separate view object.

// System balloon marker (MKMarkerAnnotationView)
MKPointAnnotation(id: MKAnnotationId('home'), coordinate: home)

// Tinted + glyph-branded marker
MKPointAnnotation(
  id: MKAnnotationId('cafe'),
  coordinate: cafe,
  icon: MKAnnotationIcon.marker(
    markerTintColor: Colors.brown,
    systemImage: 'cup.and.saucer.fill',
    glyphTintColor: Colors.white,
  ),
)

// Custom image from an asset
final icon = await MKAnnotationIcon.asset('assets/pin.png');
MKPointAnnotation(id: MKAnnotationId('shop'), coordinate: shop, icon: icon)

// Fully custom marker from a rendered Flutter widget: capture PNG bytes
// (e.g. via RenderRepaintBoundary.toImage) and pass them through:
MKPointAnnotation(
  id: MKAnnotationId('w'),
  coordinate: here,
  icon: MKAnnotationIcon.image(pngBytes),
)

// Callout + drag
MKPointAnnotation(
  id: MKAnnotationId('apple-park'),
  coordinate: applePark,
  title: 'Apple Park',
  subtitle: 'One Apple Park Way',
  onCalloutTap: () => Navigator.pushNamed(context, '/details'),
  isDraggable: true,
  onDragEnd: (coordinate) => print('dropped $coordinate'),
)

Annotations sharing a clusteringIdentifier cluster natively when they crowd.

Overlays #

MKPolyline, MKPolygon, MKCircle, MKTileOverlay — each merges its renderer's stroke/fill vocabulary (strokeColor, lineWidth, lineDashPattern):

MKMapView(
  initialCamera: ...,
  polylines: {
    MKPolyline(
      id: const MKPolylineId('route'),
      coordinates: const [...],
      strokeColor: Colors.blue,
      lineWidth: 6,
      lineCap: CGLineCap.round,
      lineJoin: CGLineJoin.round,
      lineDashPattern: const [6, 3],     // alternating dash/gap points
      gradientColors: const [Colors.green, Colors.red], // MKGradientPolylineRenderer
      level: MKOverlayLevel.aboveLabels,
    ),
    // Great-circle path, rendered by native MKGeodesicPolyline:
    MKPolyline.geodesic(
      id: const MKPolylineId('sfo-nrt'),
      coordinates: const [sfo, nrt],
    ),
  },
  polygons: {
    MKPolygon(
      id: const MKPolygonId('zone'),
      coordinates: const [...],
      interiorPolygons: const [[...]],   // holes, MKPolygon-exact
      fillColor: Colors.red.withValues(alpha: 0.3),
      strokeColor: Colors.red,
    ),
  },
  circles: {
    MKCircle(
      id: const MKCircleId('radius'),
      center: const CLLocationCoordinate2D(latitude: ..., longitude: ...),
      radius: 500, // meters
    ),
  },
)

Tile overlays go through the controller (MKTileOverlay(urlTemplate:) semantics):

await controller.addTileOverlay(
  const MKTileOverlay(
    id: MKTileOverlayId('osm'),
    urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
  ),
);

Controller — MKMapView's imperative surface #

final MKMapCamera camera = await controller.camera;          // MKMapView.camera
final MKCoordinateRegion region = await controller.region;   // MKMapView.region
await controller.setCamera(camera, animated: true);          // setCamera(_:animated:)
await controller.setRegion(region, animated: true);          // setRegion(_:animated:)
await controller.setCenter(coordinate, animated: true);      // setCenter(_:animated:)
await controller.convertToPoint(coordinate);                 // convert(_:toPointTo:)
await controller.convertToCoordinate(offset);                // convert(_:toCoordinateFrom:)
await controller.showCallout(MKAnnotationId('a'));
await controller.openLookAround(coordinate);                 // MKLookAroundViewController
final Uint8List png = await controller.takeSnapshot(
  const MKMapSnapshotOptions(showsBuildings: false),         // MKMapSnapshotter.Options
);

Mutations run on an internal serial queue — concurrent calls execute in source order. controller.dispose() runs automatically when the owning MKMapView widget unmounts.

Two dialects #

The canonical API is the MapKit mirror above. If you think in google_maps_flutter terms, the exported CameraConveniences extension layers that dialect on top — implemented purely over the canonical calls, no extra platform surface:

await controller.zoomTo(14);
await controller.zoomBy(2);
await controller.zoomIn();
await controller.scrollBy(80, -40);
await controller.fitCoordinates(annotations.map((a) => a.coordinate));
final zoom = await controller.getZoomLevel();

Zoom levels are a Web-Mercator convenience (MKMapCamera.withZoomLevel / camera.zoomLevel); MapKit's native unit is distance in meters.

Errors #

MapKit's imperative API doesn't throw, and neither do the mirrored calls. Expected conditions degrade instead: camera moves clamp, unknown ids are ignored (showCallout racing a rebuild that removed the annotation is harmless, matching selectAnnotation semantics), conversions return null before the first layout, and openLookAround returns false when no scene exists.

What can genuinely fail throws a typed MapKitException instead of a raw PlatformException:

try {
  final png = await controller.takeSnapshot();
} on MapKitPlatformException catch (e) {
  debugPrint('${e.code}: ${e.message}'); // e.g. snapshot-failed
}
  • MapKitPlatformException — native failure with a stable code (takeSnapshotsnapshot-failed) plus the native message / details.
  • MapKitDisposedException — controller used after the owning widget unmounted.
  • MapKitUnsupportedPlatformExceptionMKMapView built off iOS/macOS.

Failures MKMapView reports through its delegate surface as widget callbacks: onDidFailToLocateUser (most commonly location permission denied) and onDidFailLoadingMap (map content failed to load, e.g. offline).

Type-safe platform channel #

The Dart↔Swift boundary is generated by Pigeon from pigeons/messages.dart. After editing the schema, regenerate with dart run pigeon --input pigeons/messages.dart.

The generated files (lib/src/messages.g.dart, darwin/mapkit_flutter/Sources/mapkit_flutter/messages.g.swift) are committed and must not be hand-edited. Wire types keep a Platform prefix so the generated Swift never shadows real MapKit symbols; public Dart names are restored via typedef (e.g. MKUserTrackingMode).

Claude Code skill #

A scaffold skill ships at tool/skills/flutter-mapkit-scaffold/SKILL.md. Drop it into your user skills and invoke with /flutter-mapkit-scaffold:

cp -R tool/skills/flutter-mapkit-scaffold ~/.claude/skills/

License #

MIT — see LICENSE.

1
likes
160
points
302
downloads

Documentation

API reference

Publisher

verified publishermehmetesen.com

Weekly Downloads

MapKit for Flutter. Display MKMapView on iOS with annotations, overlays, clustering, look-around, tile overlays, and modern MapKit configurations.

Repository (GitHub)
View/report issues

Topics

#maps #ios #apple #mapkit

License

MIT (license)

Dependencies

flutter, meta

More

Packages that depend on mapkit_flutter

Packages that implement mapkit_flutter