Display a Popup in Flutter
This tutorial shows different ways to display popups and info overlays on your MapMetrics Flutter map — from simple tap-to-show cards to custom bottom sheets.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
No built-in popup widget: the SDK has no
Marker/InfoWindowpair on the map widget itself and no native popup bubble. Interactive markers are built withWidgetLayer— amapChildrenentry that places ordinary Flutter widgets (wrapped inMarker(point:, child:)) at map coordinates — combined with aGestureDetectorfor taps, and any Flutter overlay (Positionedcard,showModalBottomSheet, etc.) for the popup content itself.
Basic Tap-to-Show Popup
Place tappable pins with WidgetLayer and show a small card with the place's title and description when one is tapped:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class BasicPopupScreen extends StatefulWidget {
@override
_BasicPopupScreenState createState() => _BasicPopupScreenState();
}
class _BasicPopupScreenState extends State<BasicPopupScreen> {
MapController? mapController;
Map<String, dynamic>? selectedPlace;
final List<Map<String, dynamic>> places = [
{
'id': 'eiffel',
'name': 'Eiffel Tower',
'snippet': 'Built in 1889 — 330m tall',
'position': Position(2.2945, 48.8584),
},
{
'id': 'louvre',
'name': 'Louvre Museum',
'snippet': 'Home of the Mona Lisa',
'position': Position(2.3376, 48.8606),
},
{
'id': 'notre_dame',
'name': 'Notre-Dame Cathedral',
'snippet': 'Gothic masterpiece since 1163',
'position': Position(2.3499, 48.8530),
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Basic Popup')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(2.3200, 48.8566),
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
mapChildren: [
WidgetLayer(
allowInteraction: true,
markers: places.map((place) {
return Marker(
point: place['position'] as Position,
size: const Size.square(36),
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: () => setState(() => selectedPlace = place),
child: const Icon(
Icons.location_on,
color: Colors.red,
size: 36,
),
),
);
}).toList(),
),
],
),
if (selectedPlace != null)
Positioned(
bottom: 24,
left: 16,
right: 16,
child: Card(
child: ListTile(
title: Text(selectedPlace!['name'] as String),
subtitle: Text(selectedPlace!['snippet'] as String),
trailing: IconButton(
icon: Icon(Icons.close),
onPressed: () => setState(() => selectedPlace = null),
),
),
),
),
],
),
);
}
}Custom Popup Overlay
Show a floating card popup when tapping a marker, and dismiss it when the base map itself is tapped (via onEvent's MapEventClick):
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CustomPopupScreen extends StatefulWidget {
@override
_CustomPopupScreenState createState() => _CustomPopupScreenState();
}
class _CustomPopupScreenState extends State<CustomPopupScreen> {
MapController? 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.',
'position': Position(2.2945, 48.8584),
'rating': 4.7,
},
{
'id': 'louvre',
'name': 'Louvre Museum',
'description': 'World\'s largest art museum and historic monument.',
'position': Position(2.3376, 48.8606),
'rating': 4.8,
},
{
'id': 'sacre_coeur',
'name': 'Sacre-Coeur',
'description': 'White-domed basilica atop Montmartre hill.',
'position': Position(2.3431, 48.8867),
'rating': 4.6,
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Popup')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(2.3200, 48.8600),
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
onEvent: (event) {
if (event is MapEventClick) {
// Dismiss popup when tapping the map itself
setState(() {
selectedPlace = null;
});
}
},
mapChildren: [
WidgetLayer(allowInteraction: true, markers: _buildMarkers()),
],
),
// Custom popup card
if (selectedPlace != null)
Positioned(
bottom: 24,
left: 16,
right: 16,
child: _buildPopupCard(),
),
],
),
);
}
List<Marker> _buildMarkers() {
return places.map((place) {
final position = place['position'] as Position;
return Marker(
point: position,
size: const Size.square(36),
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: () {
setState(() {
selectedPlace = place;
});
// Center the map on the tapped marker
mapController?.animateCamera(center: position);
},
child: const Icon(Icons.location_on, color: Colors.red, size: 36),
),
);
}).toList();
}
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:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class BottomSheetPopupScreen extends StatefulWidget {
@override
_BottomSheetPopupScreenState createState() =>
_BottomSheetPopupScreenState();
}
class _BottomSheetPopupScreenState extends State<BottomSheetPopupScreen> {
MapController? 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',
'position': Position(2.2945, 48.8584),
},
{
'id': 'arc',
'name': 'Arc de Triomphe',
'address': 'Place Charles de Gaulle',
'hours': 'Open 10:00 AM - 10:30 PM',
'position': Position(2.2950, 48.8738),
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bottom Sheet Popup')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.3000, 48.8600),
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
mapChildren: [
WidgetLayer(
allowInteraction: true,
markers: landmarks.map((landmark) {
final position = landmark['position'] as Position;
return Marker(
point: position,
size: const Size.square(36),
alignment: Alignment.bottomCenter,
child: GestureDetector(
onTap: () => _showBottomSheet(landmark),
child: const Icon(
Icons.location_on,
color: Colors.red,
size: 36,
),
),
);
}).toList(),
),
],
),
);
}
void _showBottomSheet(Map<String, dynamic> landmark) {
mapController?.animateCamera(center: landmark['position'] as Position);
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
- Add a Popup — Simple marker popups
- Popup on Click — Show popups on map tap
- Markers and Annotations — Full marker guide
Tip: For production apps, use the bottom sheet approach — it feels native on mobile and provides more space for content. Use the basic tap-to-show card for quick prototypes.