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

Popup on Click in Flutter

This tutorial shows how to display a popup with detailed information when the user taps on a map feature like a marker.

Prerequisites

Before you begin, ensure you have:

How tap detection works on this SDK

There is no onMarkerTapped/onMapClick callback pair. Instead:

  • Markers are real Flutter widgets rendered via WidgetLayer/Marker in mapChildren. Because the marker's child is an ordinary widget, you wrap it in a GestureDetector to catch taps on that specific marker — set WidgetLayer(allowInteraction: true, ...) so gestures reach the markers instead of passing through to the map.
  • Taps on the map background (not on a marker) arrive through the onEvent callback as a MapEventClick, carrying the tapped Position.

Show a detail card when any marker is tapped:

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

class PopupOnClickScreen extends StatefulWidget {
  @override
  _PopupOnClickScreenState createState() => _PopupOnClickScreenState();
}

class _PopupOnClickScreenState extends State<PopupOnClickScreen> {
  MapController? mapController;
  Map<String, dynamic>? selectedPlace;

  final List<Map<String, dynamic>> places = [
    {
      'id': 'eiffel',
      'name': 'Eiffel Tower',
      'description': 'Iconic iron lattice tower built in 1889.',
      'category': 'Landmark',
      'position': Position(2.2945, 48.8584), // lng, lat
      'rating': 4.7,
      'color': Colors.red,
    },
    {
      'id': 'louvre',
      'name': 'Louvre Museum',
      'description': 'World\'s largest art museum, home to the Mona Lisa.',
      'category': 'Museum',
      'position': Position(2.3376, 48.8606),
      'rating': 4.8,
      'color': Colors.blue,
    },
    {
      'id': 'notre_dame',
      'name': 'Notre-Dame Cathedral',
      'description': 'Medieval Catholic cathedral, a masterpiece of Gothic architecture.',
      'category': 'Church',
      'position': Position(2.3499, 48.8530),
      'rating': 4.6,
      'color': Colors.deepPurple,
    },
    {
      'id': 'sacre_coeur',
      'name': 'Sacré-Cœur',
      'description': 'White-domed basilica on the highest point in Paris.',
      'category': 'Church',
      'position': Position(2.3431, 48.8867),
      'rating': 4.5,
      'color': Colors.deepPurple,
    },
  ];

  List<Marker> get markers => places.map((place) {
    return Marker(
      point: place['position'] as Position,
      size: const Size(32, 32),
      alignment: Alignment.bottomCenter,
      child: GestureDetector(
        onTap: () => setState(() => selectedPlace = place),
        child: Icon(
          Icons.location_on,
          color: place['color'] as Color,
          size: 32,
        ),
      ),
    );
  }).toList();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Tap a Marker')),
      body: Stack(
        children: [
          MapMetricsView(
            options: MapOptions(
              initStyle: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              initCenter: Position(2.3200, 48.8620), // lng, lat
              initZoom: 13.0,
            ),
            onMapCreated: (controller) => mapController = controller,
            onEvent: (event) {
              if (event is MapEventClick) {
                // Dismiss popup when tapping the map background
                setState(() => selectedPlace = null);
              }
            },
            mapChildren: [
              WidgetLayer(allowInteraction: true, markers: markers),
            ],
          ),
          // Popup card
          if (selectedPlace != null)
            Positioned(
              bottom: 24,
              left: 16,
              right: 16,
              child: Card(
                elevation: 6,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Expanded(
                            child: Text(
                              selectedPlace!['name'],
                              style: TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                          IconButton(
                            icon: Icon(Icons.close, size: 20),
                            onPressed: () =>
                                setState(() => selectedPlace = null),
                            padding: EdgeInsets.zero,
                            constraints: BoxConstraints(),
                          ),
                        ],
                      ),
                      SizedBox(height: 4),
                      Container(
                        padding:
                            EdgeInsets.symmetric(horizontal: 8, vertical: 3),
                        decoration: BoxDecoration(
                          color: Colors.blue[50],
                          borderRadius: BorderRadius.circular(12),
                        ),
                        child: Text(
                          selectedPlace!['category'],
                          style: TextStyle(fontSize: 12, color: Colors.blue),
                        ),
                      ),
                      SizedBox(height: 8),
                      Text(
                        selectedPlace!['description'],
                        style: TextStyle(
                            fontSize: 14, color: Colors.grey[700]),
                      ),
                      SizedBox(height: 8),
                      Row(
                        children: [
                          Icon(Icons.star, color: Colors.amber, size: 18),
                          SizedBox(width: 4),
                          Text(
                            '${selectedPlace!['rating']}',
                            style: TextStyle(fontWeight: FontWeight.w500),
                          ),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

Show coordinates when tapping anywhere on the map, using the onEvent/MapEventClick callback:

dart
MapMetricsView(
  // ... options
  onEvent: (event) {
    if (event is MapEventClick) {
      final coordinates = event.point; // Position, longitude first
      showModalBottomSheet(
        context: context,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
        ),
        builder: (context) => Padding(
          padding: EdgeInsets.all(20),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Tapped Location',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
              SizedBox(height: 12),
              _infoRow(Icons.location_on, 'Latitude',
                  coordinates.lat.toStringAsFixed(6)),
              _infoRow(Icons.location_on, 'Longitude',
                  coordinates.lng.toStringAsFixed(6)),
              SizedBox(height: 12),
            ],
          ),
        ),
      );
    }
  },
)

Widget _infoRow(IconData icon, String label, String value) {
  return Padding(
    padding: EdgeInsets.symmetric(vertical: 4),
    child: Row(
      children: [
        Icon(icon, size: 16, color: Colors.grey),
        SizedBox(width: 8),
        Text('$label: ', style: TextStyle(fontWeight: FontWeight.w500)),
        Text(value, style: TextStyle(fontFamily: 'monospace')),
      ],
    ),
  );
}

If you also need the screen-space Offset of the tap (not just the geographic coordinate), convert it with mapController.toScreenLocation(coordinates) — there's no separate Point argument delivered alongside the click event like in the old fictional API.

Next Steps


Tip: Dismiss the popup when the user taps on the map background by handling onEvent for MapEventClick and clearing the selected item — marker taps are absorbed by each marker's own GestureDetector and won't also trigger the background MapEventClick.