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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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
- 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 simple InfoWindow for quick prototypes.