Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Flutter — MapController

MapController is handed to you in MapMetricsView(onMapCreated: (controller) => ...) — or fetched from context with MapController.of(context) / MapController.maybeOf(context) from inside MapMetricsView's mapChildren. It's the only object you use to drive the map programmatically.

There is no onMapClick, onCameraMove, onCameraIdle, addSymbol, addMarker, SymbolManager, MapMetricsController, CameraPosition, LatLng, or MyLocationTrackingMode on this or any other type in the package. Map interaction events (clicks, camera changes) come through MapMetricsView(onEvent: (MapEvent event) => ...) instead — see MapEvent below.

All coordinates are Position(longitude, latitude) — see the coordinate order warning on the MapOptions page.

Getting a controller

dart
MapMetricsView(
  options: MapOptions(initCenter: Position(4.8952, 52.3702), initZoom: 12),
  onMapCreated: (MapController controller) {
    // store it, e.g. in a State field
  },
)

Camera

dart
Future<void> moveCamera({Position? center, double? zoom, double? bearing, double? pitch});

void moveCameraSync({Position? center, double? zoom, double? bearing, double? pitch});

Future<void> animateCamera({
  Position? center,
  double? zoom,
  double? bearing,
  double? pitch,
  Duration nativeDuration = const Duration(seconds: 2),
  double webSpeed = 1.2,
  Duration? webMaxDuration,
});

Future<void> fitBounds({
  required LngLatBounds bounds,
  double? bearing,
  double? pitch,
  Duration nativeDuration = const Duration(seconds: 2),
  double webSpeed = 1.2,
  Duration? webMaxDuration,
  Offset offset = Offset.zero,
  double webMaxZoom = double.maxFinite,
  bool webLinear = false,
  EdgeInsets padding = EdgeInsets.zero,
});

MapCamera getCamera();
MapCamera? get camera;
  • moveCamera jumps instantly; all parameters are optional — omit one to leave it unchanged.
  • moveCameraSync is the same but synchronous (Android JNI only; falls back to async moveCamera on other platforms). Use it in tight render loops to avoid microtask scheduling gaps.
  • animateCamera eases to the new position over nativeDuration (native) or at webSpeed (web, capped by webMaxDuration).
  • fitBounds animates the camera to frame a LngLatBounds, with optional padding/offset.
  • getCamera() / camera return the current MapCamera (center, zoom, bearing, pitch).
dart
await controller.animateCamera(
  center: Position(2.3522, 48.8566), // Paris: lng, lat
  zoom: 13,
  nativeDuration: const Duration(milliseconds: 1500),
);

await controller.fitBounds(
  bounds: const LngLatBounds(
    longitudeWest: 9.589786,
    longitudeEast: 9.766498,
    latitudeSouth: 47.446159,
    latitudeNorth: 47.574776,
  ),
  padding: const EdgeInsets.all(32),
);

Also on MapController:

dart
Future<double> getMetersPerPixelAtLatitude(double latitude);
double getMetersPerPixelAtLatitudeSync(double latitude);
Future<LngLatBounds> getVisibleRegion();
LngLatBounds getVisibleRegionSync();

Projection

dart
Future<Offset> toScreenLocation(Position lngLat);
Future<Position> toLngLat(Offset screenLocation);
Future<List<Offset>> toScreenLocations(List<Position> lngLats);
Future<List<Position>> toLngLats(List<Offset> screenLocations);

Offset toScreenLocationSync(Position lngLat);
Position toLngLatSync(Offset screenLocation);
List<Offset> toScreenLocationsSync(List<Position> lngLats);
List<Position> toLngLatsSync(List<Offset> screenLocations);

Async and sync forms exist for both single values and lists — use the plural (toScreenLocations/toLngLats) when converting several points at once instead of looping the singular form.

dart
final screenPoint = await controller.toScreenLocation(Position(4.8952, 52.3702));
final geoPoint = await controller.toLngLat(const Offset(150, 300));

Querying rendered features

dart
Future<List<Map<String, String>>> queryLayers(Offset screenLocation);
Future<List<Map<String, String>>> queryLayersInRect(Rect rect);

Both return the properties of every rendered feature at the point (or within the rectangle), including layer/source metadata keys. queryLayersInRect is more efficient than repeated point queries when you need hit detection over an area.

dart
Future<void> _handleTap(Offset screenLocation) async {
  final features = await controller.queryLayers(screenLocation);
  final poiHits = features.where((f) => f['layerId'] == 'poi-symbols');
  // ...
}

User location

dart
Future<void> enableLocation({
  Duration fastestInterval = const Duration(milliseconds: 750),
  Duration maxWaitTime = const Duration(seconds: 1),
  bool pulseFade = true,
  bool accuracyAnimation = true,
  bool compassAnimation = true,
  bool pulse = true,
});

Future<void> trackLocation({
  bool trackLocation = true,
  BearingTrackMode trackBearing = BearingTrackMode.gps,
});

Future<void> showUserLocationPuck({bool show = true});
Future<void> setLocationDraggable({bool draggable = true});
  • enableLocation starts showing the user location puck (blue dot).
  • trackLocation re-centers the camera on the user's position as it updates; trackBearing controls whether/how the camera bearing follows (BearingTrackMode.none, .compass, or .gps).
  • showUserLocationPuck toggles puck visibility without disabling tracking.
  • setLocationDraggable lets the user tap-drag the puck to a new position once tapped into drag mode.

Runtime location permissions must be granted first — see Location & Permissions.

dart
await controller.enableLocation();
await controller.trackLocation(trackBearing: BearingTrackMode.gps);
dart
Future<void> setNavigationRoute(List<Position> routePoints);
Future<void> clearNavigationRoute();

void navigateFrame({
  required Position center,
  required double zoom,
  required double bearing,
  required double pitch,
  required String sourceId,
  required String geoJsonData,
});

setNavigationRoute gives the location puck a route line to snap to, improving on-road accuracy while navigating. navigateFrame is a single synchronous call that moves the camera and updates a GeoJSON source together in one native frame — use it in a navigation ticker to avoid camera/marker desync that two separate calls (moveCameraSync + a style source update) would introduce.

Style

dart
Future<void> setStyleUri(String styleUri);

Switches the map style in place without destroying and recreating the native map view (avoiding a crash class that can occur when GeoJSON messages are queued on the native looper during teardown). MapMetricsView(onStyleLoaded: ...) fires again once the new style has finished loading.

dart
await controller.setStyleUri(
  'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/dark.json&token=YOUR_API_KEY',
);

Other members

dart
MapOptions get options;      // the MapOptions this map was created with
StyleController? get style;  // null until onStyleLoaded has fired

Map events

MapController itself has no click/camera-move callbacks. Instead, MapMetricsView(onEvent: (MapEvent event) => ...) delivers a sealed MapEvent hierarchy: MapEventStyleLoaded, MapEventMapCreated, MapEventMoveCamera, MapEventStartMoveCamera (carries a CameraChangeReason), MapEventClick, MapEventDoubleClick, MapEventSecondaryClick, MapEventLongClick (all carry a Position point), MapEventIdle, and MapEventCameraIdle.

dart
MapMetricsView(
  options: MapOptions(initCenter: Position(4.8952, 52.3702), initZoom: 12),
  onEvent: (event) {
    switch (event) {
      case MapEventClick(:final point):
        debugPrint('tapped at $point');
      case MapEventCameraIdle():
        debugPrint('camera settled');
      default:
        break;
    }
  },
)

See also