Skip to content

Display a Popup in Flutter

This tutorial shows different ways to display popups and info overlays on your MapMetrics Flutter map — from simple info windows to custom bottom sheets.

Prerequisites

Before you begin, ensure you have:

Basic Info Window Popup

The simplest popup uses the built-in InfoWindow on markers:

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

class BasicPopupScreen extends StatefulWidget {
  @override
  _BasicPopupScreenState createState() => _BasicPopupScreenState();
}

class _BasicPopupScreenState extends State<BasicPopupScreen> {
  MapMetricsController? mapController;

  final Set<Marker> markers = {
    Marker(
      markerId: MarkerId('eiffel'),
      position: LatLng(48.8584, 2.2945),
      infoWindow: InfoWindow(
        title: 'Eiffel Tower',
        snippet: 'Built in 1889 — 330m tall',
      ),
    ),
    Marker(
      markerId: MarkerId('louvre'),
      position: LatLng(48.8606, 2.3376),
      infoWindow: InfoWindow(
        title: 'Louvre Museum',
        snippet: 'Home of the Mona Lisa',
      ),
    ),
    Marker(
      markerId: MarkerId('notre_dame'),
      position: LatLng(48.8530, 2.3499),
      infoWindow: InfoWindow(
        title: 'Notre-Dame Cathedral',
        snippet: 'Gothic masterpiece since 1163',
      ),
    ),
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Basic Popup')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: LatLng(48.8566, 2.3200),
          zoom: 13.0,
        ),
        markers: markers,
      ),
    );
  }
}

Custom Popup Overlay

Show a floating card popup when tapping a marker:

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

class CustomPopupScreen extends StatefulWidget {
  @override
  _CustomPopupScreenState createState() => _CustomPopupScreenState();
}

class _CustomPopupScreenState extends State<CustomPopupScreen> {
  MapMetricsController? mapController;
  Map<String, dynamic>? selectedPlace;

  final List<Map<String, dynamic>> places = [
    {
      'id': 'eiffel',
      'name': 'Eiffel Tower',
      'description': 'Iconic iron lattice tower on the Champ de Mars.',
      'lat': 48.8584,
      'lng': 2.2945,
      'rating': 4.7,
    },
    {
      'id': 'louvre',
      'name': 'Louvre Museum',
      'description': 'World\'s largest art museum and historic monument.',
      'lat': 48.8606,
      'lng': 2.3376,
      'rating': 4.8,
    },
    {
      'id': 'sacre_coeur',
      'name': 'Sacre-Coeur',
      'description': 'White-domed basilica atop Montmartre hill.',
      'lat': 48.8867,
      'lng': 2.3431,
      'rating': 4.6,
    },
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Custom Popup')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl:
                'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (MapMetricsController controller) {
              mapController = controller;
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(48.8600, 2.3200),
              zoom: 13.0,
            ),
            markers: _buildMarkers(),
            onMapClick: (Point point, LatLng coordinates) {
              // Dismiss popup when tapping the map
              setState(() {
                selectedPlace = null;
              });
            },
          ),
          // Custom popup card
          if (selectedPlace != null)
            Positioned(
              bottom: 24,
              left: 16,
              right: 16,
              child: _buildPopupCard(),
            ),
        ],
      ),
    );
  }

  Set<Marker> _buildMarkers() {
    return places.map((place) {
      return Marker(
        markerId: MarkerId(place['id']),
        position: LatLng(place['lat'], place['lng']),
        onTap: () {
          setState(() {
            selectedPlace = place;
          });
          // Center the map on the tapped marker
          mapController?.animateCamera(
            CameraUpdate.newLatLng(LatLng(place['lat'], place['lng'])),
          );
        },
      );
    }).toSet();
  }

  Widget _buildPopupCard() {
    return Card(
      elevation: 8,
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    selectedPlace!['name'],
                    style: TextStyle(
                      fontSize: 18,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                IconButton(
                  icon: Icon(Icons.close),
                  onPressed: () {
                    setState(() {
                      selectedPlace = null;
                    });
                  },
                ),
              ],
            ),
            SizedBox(height: 4),
            Row(
              children: [
                Icon(Icons.star, color: Colors.amber, size: 18),
                SizedBox(width: 4),
                Text('${selectedPlace!['rating']}'),
              ],
            ),
            SizedBox(height: 8),
            Text(
              selectedPlace!['description'],
              style: TextStyle(color: Colors.grey[700]),
            ),
            SizedBox(height: 12),
            Row(
              children: [
                ElevatedButton.icon(
                  onPressed: () {
                    // Handle directions action
                  },
                  icon: Icon(Icons.directions, size: 18),
                  label: Text('Directions'),
                ),
                SizedBox(width: 8),
                OutlinedButton.icon(
                  onPressed: () {
                    // Handle share action
                  },
                  icon: Icon(Icons.share, size: 18),
                  label: Text('Share'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Bottom Sheet Popup

Use a bottom sheet for more detailed information:

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

class BottomSheetPopupScreen extends StatefulWidget {
  @override
  _BottomSheetPopupScreenState createState() =>
      _BottomSheetPopupScreenState();
}

class _BottomSheetPopupScreenState extends State<BottomSheetPopupScreen> {
  MapMetricsController? mapController;

  final List<Map<String, dynamic>> landmarks = [
    {
      'id': 'eiffel',
      'name': 'Eiffel Tower',
      'address': 'Champ de Mars, 5 Av. Anatole France',
      'hours': 'Open 9:30 AM - 11:45 PM',
      'lat': 48.8584,
      'lng': 2.2945,
    },
    {
      'id': 'arc',
      'name': 'Arc de Triomphe',
      'address': 'Place Charles de Gaulle',
      'hours': 'Open 10:00 AM - 10:30 PM',
      'lat': 48.8738,
      'lng': 2.2950,
    },
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bottom Sheet Popup')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: LatLng(48.8600, 2.3000),
          zoom: 13.0,
        ),
        markers: landmarks.map((landmark) {
          return Marker(
            markerId: MarkerId(landmark['id']),
            position: LatLng(landmark['lat'], landmark['lng']),
            onTap: () => _showBottomSheet(landmark),
          );
        }).toSet(),
      ),
    );
  }

  void _showBottomSheet(Map<String, dynamic> landmark) {
    mapController?.animateCamera(
      CameraUpdate.newLatLng(LatLng(landmark['lat'], landmark['lng'])),
    );

    showModalBottomSheet(
      context: context,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
      ),
      builder: (context) {
        return Padding(
          padding: EdgeInsets.all(20),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Center(
                child: Container(
                  width: 40,
                  height: 4,
                  decoration: BoxDecoration(
                    color: Colors.grey[300],
                    borderRadius: BorderRadius.circular(2),
                  ),
                ),
              ),
              SizedBox(height: 16),
              Text(
                landmark['name'],
                style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 8),
              Row(
                children: [
                  Icon(Icons.location_on, size: 16, color: Colors.grey),
                  SizedBox(width: 4),
                  Text(landmark['address'],
                      style: TextStyle(color: Colors.grey[600])),
                ],
              ),
              SizedBox(height: 4),
              Row(
                children: [
                  Icon(Icons.access_time, size: 16, color: Colors.green),
                  SizedBox(width: 4),
                  Text(landmark['hours'],
                      style: TextStyle(color: Colors.green)),
                ],
              ),
              SizedBox(height: 16),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: () => Navigator.pop(context),
                  child: Text('Get Directions'),
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

Next Steps


Tip: For production apps, use the bottom sheet approach — it feels native on mobile and provides more space for content. Use simple InfoWindow for quick prototypes.