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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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:
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:
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:
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):
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
- Add a Polyline — Draw lines and routes on the map
- Add a Cluster — Group nearby markers
- Add Animated Icon — Animate marker widgets
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.