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

Add a Popup in Flutter

This tutorial shows how to display popups (info windows) on markers in your MapMetrics Flutter map. Popups are great for showing extra information when a user taps on a marker.

Prerequisites

Before you begin, ensure you have:

There's No Built-in InfoWindow

MapMetrics Flutter doesn't have a marker infoWindow/InfoWindow property. Markers are placed with WidgetLayer + Marker, and since a Marker.child is a real Flutter widget, "popups" are just another widget you show or hide with normal setState — no bitmap generation, no platform-specific info window API.

Basic Popup on Tap

Toggle a small card above the marker when it's tapped:

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

class PopupExampleScreen extends StatefulWidget {
  const PopupExampleScreen({super.key});

  @override
  State<PopupExampleScreen> createState() => _PopupExampleScreenState();
}

class _PopupExampleScreenState extends State<PopupExampleScreen> {
  MapController? mapController;
  bool _showPopup = false;

  static const _paris = Position(2.349902, 48.852966);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Popup Example')),
      body: MapMetricsView(
        options: MapOptions(
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
          initCenter: _paris,
          initZoom: 13,
        ),
        onMapCreated: (controller) => mapController = controller,
        mapChildren: [
          WidgetLayer(
            allowInteraction: true,
            markers: [
              Marker(
                point: _paris,
                size: const Size(160, 90),
                alignment: Alignment.bottomCenter,
                child: GestureDetector(
                  onTap: () => setState(() => _showPopup = !_showPopup),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      if (_showPopup)
                        Container(
                          margin: const EdgeInsets.only(bottom: 4),
                          padding: const EdgeInsets.symmetric(
                            horizontal: 10,
                            vertical: 6,
                          ),
                          decoration: BoxDecoration(
                            color: Colors.white,
                            borderRadius: BorderRadius.circular(8),
                            boxShadow: const [
                              BoxShadow(color: Colors.black26, blurRadius: 6),
                            ],
                          ),
                          child: const Column(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Text(
                                'Hello MapMetrics',
                                style: TextStyle(fontWeight: FontWeight.bold),
                              ),
                              Text(
                                'A good coffee shop',
                                style: TextStyle(fontSize: 12),
                              ),
                            ],
                          ),
                        ),
                      const Icon(Icons.location_on, color: Colors.red, size: 36),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Tap the marker to show the popup card; tap it again to hide it.

Multiple Markers with Popups

Track which marker's popup is open by id, so only one shows at a time:

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

class MultiplePopupsScreen extends StatefulWidget {
  const MultiplePopupsScreen({super.key});

  @override
  State<MultiplePopupsScreen> createState() => _MultiplePopupsScreenState();
}

class _MultiplePopupsScreenState extends State<MultiplePopupsScreen> {
  MapController? mapController;
  String? _openMarkerId;

  final _landmarks = const {
    'eiffel_tower': (
      title: 'Eiffel Tower',
      snippet: 'Iconic iron lattice tower on the Champ de Mars',
      point: Position(2.2945, 48.8584),
      color: Colors.blue,
    ),
    'louvre': (
      title: 'Louvre Museum',
      snippet: "World's largest art museum and historic monument",
      point: Position(2.3376, 48.8606),
      color: Colors.green,
    ),
    'notre_dame': (
      title: 'Notre-Dame Cathedral',
      snippet: 'Medieval Catholic cathedral on the Ile de la Cite',
      point: Position(2.3499, 48.8530),
      color: Colors.orange,
    ),
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Paris Landmarks')),
      body: MapMetricsView(
        options: MapOptions(
          initStyle:
              'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
          initCenter: Position(2.3222, 48.8566),
          initZoom: 13,
        ),
        onMapCreated: (controller) => mapController = controller,
        mapChildren: [
          WidgetLayer(
            allowInteraction: true,
            markers: _landmarks.entries.map((entry) {
              final id = entry.key;
              final data = entry.value;
              final isOpen = _openMarkerId == id;
              return Marker(
                point: data.point,
                size: const Size(170, 90),
                alignment: Alignment.bottomCenter,
                child: GestureDetector(
                  onTap: () => setState(
                    () => _openMarkerId = isOpen ? null : id,
                  ),
                  child: Column(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      if (isOpen)
                        Container(
                          margin: const EdgeInsets.only(bottom: 4),
                          padding: const EdgeInsets.symmetric(
                            horizontal: 10,
                            vertical: 6,
                          ),
                          decoration: BoxDecoration(
                            color: Colors.white,
                            borderRadius: BorderRadius.circular(8),
                            boxShadow: const [
                              BoxShadow(color: Colors.black26, blurRadius: 6),
                            ],
                          ),
                          child: Column(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Text(
                                data.title,
                                style: const TextStyle(
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              Text(
                                data.snippet,
                                style: const TextStyle(fontSize: 11),
                                textAlign: TextAlign.center,
                              ),
                            ],
                          ),
                        ),
                      Icon(Icons.location_on, color: data.color, size: 32),
                    ],
                  ),
                ),
              );
            }).toList(),
          ),
        ],
      ),
    );
  }
}

Custom Popup with Bottom Sheet

For richer content — images, buttons, multi-line layouts — show a showModalBottomSheet instead of an on-map card:

dart
final Map<String, Map<String, String>> markerData = {
  'cafe': {
    'title': 'Le Petit Cafe',
    'description': 'A cozy Parisian cafe with excellent croissants and espresso.',
    'hours': 'Mon-Sat: 7:00 AM - 9:00 PM',
    'rating': '4.5',
  },
  'bookstore': {
    'title': 'Shakespeare & Company',
    'description': 'Historic English-language bookstore on the Left Bank.',
    'hours': 'Daily: 10:00 AM - 10:00 PM',
    'rating': '4.8',
  },
};

void _showCustomPopup(String markerId) {
  final data = markerData[markerId];
  if (data == null) return;

  showModalBottomSheet(
    context: context,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
    ),
    builder: (context) {
      return Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              data['title']!,
              style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              data['description']!,
              style: TextStyle(fontSize: 14, color: Colors.grey[700]),
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const Icon(Icons.access_time, size: 16, color: Colors.grey),
                const SizedBox(width: 4),
                Text(data['hours']!, style: const TextStyle(fontSize: 13)),
              ],
            ),
            const SizedBox(height: 8),
            Row(
              children: [
                const Icon(Icons.star, size: 16, color: Colors.amber),
                const SizedBox(width: 4),
                Text(data['rating']!, style: const TextStyle(fontSize: 13)),
              ],
            ),
          ],
        ),
      );
    },
  );
}

Wire it up with the same GestureDetector.onTap pattern used above — call _showCustomPopup('cafe') from the marker's onTap instead of toggling an inline card.

Show a Popup on Map Tap

To react to taps anywhere on the map (not just on a marker), use onEvent and match MapEventClick, which carries the tapped Position (longitude first):

dart
MapMetricsView(
  options: MapOptions(
    initStyle:
        'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
    initCenter: Position(2.349902, 48.852966),
    initZoom: 13,
  ),
  onMapCreated: (controller) => mapController = controller,
  onEvent: (event) {
    if (event case MapEventClick(:final point)) {
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('Location'),
          content: Text(
            'Lat: ${point.lat.toStringAsFixed(6)}\n'
            'Lng: ${point.lng.toStringAsFixed(6)}',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('OK'),
            ),
          ],
        ),
      );
    }
  },
)

Next Steps


Tip: For simple text, an inline card toggled by GestureDetector.onTap (as above) feels closest to a traditional info window. For richer content with images, buttons, or custom layouts, use a showModalBottomSheet or showDialog instead — since Marker.child is a plain widget, you're not limited to a fixed info-window shape at all.