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
MapMetricsView(
options: MapOptions(initCenter: Position(4.8952, 52.3702), initZoom: 12),
onMapCreated: (MapController controller) {
// store it, e.g. in a State field
},
)Camera
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;moveCamerajumps instantly; all parameters are optional — omit one to leave it unchanged.moveCameraSyncis the same but synchronous (Android JNI only; falls back to asyncmoveCameraon other platforms). Use it in tight render loops to avoid microtask scheduling gaps.animateCameraeases to the new position overnativeDuration(native) or atwebSpeed(web, capped bywebMaxDuration).fitBoundsanimates the camera to frame aLngLatBounds, with optionalpadding/offset.getCamera()/camerareturn the currentMapCamera(center,zoom,bearing,pitch).
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:
Future<double> getMetersPerPixelAtLatitude(double latitude);
double getMetersPerPixelAtLatitudeSync(double latitude);
Future<LngLatBounds> getVisibleRegion();
LngLatBounds getVisibleRegionSync();Projection
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.
final screenPoint = await controller.toScreenLocation(Position(4.8952, 52.3702));
final geoPoint = await controller.toLngLat(const Offset(150, 300));Querying rendered features
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.
Future<void> _handleTap(Offset screenLocation) async {
final features = await controller.queryLayers(screenLocation);
final poiHits = features.where((f) => f['layerId'] == 'poi-symbols');
// ...
}User location
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});enableLocationstarts showing the user location puck (blue dot).trackLocationre-centers the camera on the user's position as it updates;trackBearingcontrols whether/how the camera bearing follows (BearingTrackMode.none,.compass, or.gps).showUserLocationPucktoggles puck visibility without disabling tracking.setLocationDraggablelets 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.
await controller.enableLocation();
await controller.trackLocation(trackBearing: BearingTrackMode.gps);Navigation route snapping
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
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.
await controller.setStyleUri(
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/dark.json&token=YOUR_API_KEY',
);Other members
MapOptions get options; // the MapOptions this map was created with
StyleController? get style; // null until onStyleLoaded has firedMap 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.
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
- MapOptions reference — the object that configures the map this controller belongs to
- Location & Permissions —
PermissionManagerpaired withenableLocation/trackLocation - Offline maps — downloading regions ahead of using
setStyleUri/moveCameraoffline