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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
How tap detection works on this SDK
There is no onMarkerTapped/onMapClick callback pair. Instead:
- Markers are real Flutter widgets rendered via
WidgetLayer/MarkerinmapChildren. Because the marker'schildis an ordinary widget, you wrap it in aGestureDetectorto catch taps on that specific marker — setWidgetLayer(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
onEventcallback as aMapEventClick, carrying the tappedPosition.
Popup on Marker Tap
Show a detail card when any marker is tapped:
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),
),
],
),
],
),
),
),
),
],
),
);
}
}Popup on Map Tap (Any Location)
Show coordinates when tapping anywhere on the map, using the onEvent/MapEventClick callback:
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
- Popup on Long Press — Long-press context menus
- Multiple Geometries — Markers, lines, polygons, circles together
- Navigation Controls — Full interaction handling
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.