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

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:

dart
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:

dart
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:

dart
// 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:

dart
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:

dart
// 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:

dart
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:

dart
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:

dart
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):

dart
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

  1. Reuse Controller: Store the MapController in a variable to avoid re-fetching it on every rebuild
  2. Debounce Events: Use debouncing for frequent events like MapEventMoveCamera delivered through onEvent
  3. Lazy Loading: Load map data (sources/layers) only when needed, typically inside onStyleLoaded
  4. Memory Management: Clean up any StyleController resources you allocated when the widget is disposed
dart
@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:

dart
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.