Basic Map with Flutter and MapMetrics
This tutorial will show you how to create a basic interactive map using Flutter and MapMetrics Atlas API.
Basic Map Implementation
Here's a complete example of a basic map with common interactions:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class BasicMapScreen extends StatefulWidget {
@override
_BasicMapScreenState createState() => _BasicMapScreenState();
}
class _BasicMapScreenState extends State<BasicMapScreen> {
MapController? mapController;
Position? lastTappedLocation;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Basic MapMetrics Map'),
actions: [
IconButton(
icon: Icon(Icons.my_location),
onPressed: _goToUserLocation,
),
],
),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(-74.0060, 40.7128), // New York City (lng, lat)
initZoom: 10.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
setState(() {
mapController = controller;
});
// Show and track the user's location.
controller.enableLocation();
controller.showUserLocationPuck();
controller.trackLocation(trackBearing: BearingTrackMode.gps);
},
onStyleLoaded: (StyleController style) {
print('Map style loaded successfully!');
},
onEvent: (MapEvent event) {
if (event is MapEventClick) {
setState(() {
lastTappedLocation = event.point;
});
_showLocationInfo(event.point);
}
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: "zoomIn",
onPressed: _zoomIn,
child: Icon(Icons.add),
),
SizedBox(height: 8),
FloatingActionButton(
heroTag: "zoomOut",
onPressed: _zoomOut,
child: Icon(Icons.remove),
),
],
),
);
}
void _zoomIn() {
final camera = mapController?.camera;
if (camera != null) {
mapController?.animateCamera(zoom: camera.zoom + 1);
}
}
void _zoomOut() {
final camera = mapController?.camera;
if (camera != null) {
mapController?.animateCamera(zoom: camera.zoom - 1);
}
}
void _goToUserLocation() {
mapController?.animateCamera(zoom: 15.0);
}
void _showLocationInfo(Position coordinates) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Location Information'),
content: Text(
'Latitude: ${coordinates.lat.toStringAsFixed(6)}\n'
'Longitude: ${coordinates.lng.toStringAsFixed(6)}',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
);
},
);
}
}Map Configuration Options
Initial Camera
Control the initial view of your map via MapOptions — there is no separate CameraPosition object, the values live directly on the options passed to MapMetricsView:
MapOptions(
initCenter: Position(-74.0060, 40.7128), // Center point (lng, lat)
initZoom: 10.0, // Zoom level (0-22)
initBearing: 0.0, // Rotation in degrees
initPitch: 0.0, // Tilt in degrees
),User Location
There is no myLocationEnabled / myLocationTrackingMode / myLocationRenderMode boolean flags on the widget. User location is controlled imperatively through the MapController once the map is created:
// Turns on the location engine and shows the blue dot.
await mapController?.enableLocation();
await mapController?.showUserLocationPuck(show: true);
// Keep the camera (and optionally bearing) following the user.
await mapController?.trackLocation(
trackLocation: true,
trackBearing: BearingTrackMode.gps, // .none or .compass also available
);Map Interactions
There is no onMapClick / onMapLongClick / onCameraIdle callback on MapMetricsView. All user-interaction and camera-lifecycle events arrive through a single onEvent: (MapEvent event) callback, and you switch on the event's runtime type:
onMapCreated: (MapController controller) {
// Called when the native map view has been created.
},
onStyleLoaded: (StyleController style) {
// Called when the map style has finished loading.
},
onEvent: (MapEvent event) {
switch (event) {
case MapEventClick():
// Called when the user taps the map. event.point is a Position.
break;
case MapEventLongClick():
// Called when the user long-presses the map.
break;
case MapEventCameraIdle():
// Called when the camera stops moving.
break;
default:
break;
}
},Camera Controls
Programmatic Camera Movement
There is no CameraUpdate builder class — moveCamera (instant) and animateCamera (animated) take the target values directly:
// Zoom to a specific level (relative changes need the current camera first).
final camera = mapController?.camera;
if (camera != null) {
await mapController?.animateCamera(zoom: 15.0);
// "Zoom in" / "zoom out" by one level.
await mapController?.animateCamera(zoom: camera.zoom + 1);
await mapController?.animateCamera(zoom: camera.zoom - 1);
}
// Move to a specific location.
await mapController?.animateCamera(
center: Position(-122.4194, 37.7749), // San Francisco (lng, lat)
);
// Move with zoom.
await mapController?.animateCamera(
center: Position(-122.4194, 37.7749),
zoom: 15.0,
);
// Fit bounds.
await mapController?.fitBounds(
bounds: const LngLatBounds(
longitudeWest: -122.5,
longitudeEast: -122.4,
latitudeSouth: 37.7,
latitudeNorth: 37.8,
),
padding: const EdgeInsets.all(50),
);Camera State
There is no separate onCameraMove callback — camera movement and idle notifications come through onEvent as MapEventMoveCamera / MapEventCameraIdle:
onEvent: (MapEvent event) {
if (event is MapEventMoveCamera) {
print('Camera moving: ${event.camera.zoom}');
} else if (event is MapEventCameraIdle) {
print('Camera stopped moving');
}
},Map State Management
Get Current Camera Position
There is no async "get camera position" method — camera state is exposed as MapCamera via the synchronous getCamera() method or the camera getter:
void _getCurrentPosition() {
final camera = mapController?.camera; // or mapController?.getCamera()
if (camera != null) {
print('Current zoom: ${camera.zoom}');
print('Current center: Position(${camera.center.lng}, ${camera.center.lat})');
}
}Check Map State
There is no isMapReady() method. The map is ready as soon as onMapCreated has fired — track that with your own flag instead:
bool _mapReady = false;
// in onMapCreated:
onMapCreated: (controller) {
mapController = controller;
setState(() => _mapReady = true);
},Error Handling
There is no onError callback on MapMetricsView. Wrap individual asynchronous controller/style calls in try/catch instead — this matches how the SDK itself reports failures (e.g. PlatformException from animateCamera, fitBounds):
Future<void> _moveCameraSafely() async {
try {
await mapController?.animateCamera(
center: Position(-74.0060, 40.7128),
zoom: 12,
);
} on PlatformException catch (error) {
print('Map error: ${error.code} ${error.message}');
// Show error message to user
}
}Performance Tips
- Reuse Controller: Store the
MapControllerin a variable to avoid re-fetching it on every rebuild - Debounce Events: Use debouncing for frequent events like
MapEventMoveCameradelivered throughonEvent - Lazy Loading: Load map data (sources/layers) only when needed, typically inside
onStyleLoaded - Memory Management: Clean up any
StyleControllerresources you allocated when the widget is disposed
@override
void dispose() {
// MapController itself has no dispose() — it's owned by the widget tree.
// If you kept a StyleController reference and added custom resources
// (timers, subscriptions), clean those up here instead.
super.dispose();
}Custom Map Controls
Create custom floating action buttons for map controls:
Widget _buildMapControls() {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.small(
heroTag: "zoomIn",
onPressed: _zoomIn,
child: Icon(Icons.add),
),
SizedBox(height: 8),
FloatingActionButton.small(
heroTag: "zoomOut",
onPressed: _zoomOut,
child: Icon(Icons.remove),
),
SizedBox(height: 8),
FloatingActionButton.small(
heroTag: "location",
onPressed: _goToUserLocation,
child: Icon(Icons.my_location),
),
],
);
}Next Steps
Now that you have a basic map working, try:
Pro Tip: Use the MapMetrics Portal to create custom map styles that match your app's design. You can customize colors, fonts, and which map features are displayed.