This guide shows you how to integrate the MapMetrics Flutter package with POI functionality into your own Flutter application.
Add MapMetrics to your pubspec.yaml:
dependencies:
flutter:
sdk: flutter
mapmetrics:
git:
url: https://github.com/MapMetrics/MapMetrics-flutter.git
# Or use local path for development:
# path: /path/to/MapMetrics-flutterThen run:
flutter pub getCreate a basic map page in your app:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class MyMapPage extends StatefulWidget {
const MyMapPage({super.key});
@override
State<MyMapPage> createState() => _MyMapPageState();
}
class _MyMapPageState extends State<MyMapPage> {
MapLibreMapController? _mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My Map')),
body: MapLibreMap(
options: MapOptions(
initZoom: 14,
initCenter: Position(9.17, 45.46), // Your desired location
initStyle: 'https://demotiles.maplibre.org/style.json',
),
onMapCreated: (controller) {
_mapController = controller;
// Initialize POI layers after map is ready
_initializePOILayers();
},
onLongClick: _handleLongPress,
),
);
}
Future<void> _initializePOILayers() async {
// Will be implemented in next step
}
Future<void> _handleLongPress(Position point) async {
// Will be implemented in next step
}
}import 'package:http/http.dart' as http;
import 'dart:convert';
class _MyMapPageState extends State<MyMapPage> {
MapLibreMapController? _mapController;
StyleController? _styleController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('My Map with POIs')),
body: MapLibreMap(
options: MapOptions(
initZoom: 14,
initCenter: Position(9.17, 45.46),
initStyle: 'https://demotiles.maplibre.org/style.json',
),
onMapCreated: (controller) {
_mapController = controller;
},
onStyleLoaded: (controller) async {
_styleController = controller;
await _setupPOILayers();
},
onLongClick: _handleLongPress,
),
);
}
Future<void> _setupPOILayers() async {
if (_styleController == null) return;
try {
// 1. Add POI vector tile source
await _styleController!.addVectorSource(
id: 'poi-source',
tiles: ['https://your-poi-tile-server.com/tiles/{z}/{x}/{y}.mvt'],
minZoom: 0,
maxZoom: 22,
);
// 2. Load POI icons
await _loadPOIIcons();
// 3. Add POI symbol layer with icon expressions
await _styleController!.addSymbolLayer(
id: 'poi-symbols',
sourceId: 'poi-source',
layout: {
'source-layer': 'pois',
'icon-image': [
'case',
// Restaurant POIs
['==', ['get', 'amenity'], 'restaurant'],
'restaurant_icon',
// Cafe POIs
['==', ['get', 'amenity'], 'cafe'],
'cafe_icon',
// Default fallback
'default_icon',
],
'icon-size': 0.8,
'icon-allow-overlap': true,
'text-field': ['get', 'name'],
'text-size': 12,
'text-offset': [0, 1.5],
'text-anchor': 'top',
},
paint: {
'text-color': '#333333',
'text-halo-color': '#FFFFFF',
'text-halo-width': 2,
},
);
print('POI layers added successfully');
} catch (e) {
print('Error setting up POI layers: $e');
}
}
Future<void> _loadPOIIcons() async {
// Load your POI icons from assets or network
// Example:
final iconData = await rootBundle.load('assets/icons/restaurant.png');
final bytes = iconData.buffer.asUint8List();
await _styleController!.addImage('restaurant_icon', bytes);
// Load other icons similarly...
}
Future<void> _handleLongPress(Position point) async {
if (_mapController == null) return;
try {
// Convert geographic position to screen coordinates
final screenLocation = await _mapController!.toScreenLocation(point);
// Query layers at that location
final layers = await _mapController!.queryLayers(screenLocation);
// Filter for POI layers
final poiFeatures = layers.where((feature) {
final layerId = feature['layerId'];
return layerId == 'poi-symbols';
}).toList();
if (poiFeatures.isNotEmpty) {
final poiData = poiFeatures.first;
await _showPOIDialog(poiData);
}
} catch (e) {
print('Error handling long press: $e');
}
}
Future<void> _showPOIDialog(Map<String, String> poiData) async {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(poiData['name'] ?? 'POI Information'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: poiData.entries.map((entry) {
// Skip metadata keys
if (['layerId', 'sourceId', 'sourceLayer'].contains(entry.key)) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${entry.key}: ',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Expanded(child: Text(entry.value)),
],
),
);
}).toList(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
}For more complex POI setups with multiple categories (like the example app), you can:
class POIService {
final StyleController styleController;
POIService(this.styleController);
// POI category definitions
static const List<String> categories = [
'landuse', 'power', 'barrier', 'emergency', 'man_made',
'public_transport', 'highway', 'railway', 'aeroway', 'natural',
'historic', 'healthcare', 'craft', 'office', 'sport',
'leisure', 'tourism', 'shop', 'amenity',
];
Future<void> setupPOIs({
required String sourceId,
required String tileUrl,
required Map<String, Uint8List> iconImages,
}) async {
// Add source
await styleController.addVectorSource(
id: sourceId,
tiles: [tileUrl],
minZoom: 0,
maxZoom: 22,
);
// Add all icon images
for (final entry in iconImages.entries) {
await styleController.addImage(entry.key, entry.value);
}
// Build complex icon-image expression
final iconExpression = _buildIconExpression();
// Add symbol layer
await styleController.addSymbolLayer(
id: 'poi-symbols',
sourceId: sourceId,
layout: {
'source-layer': 'pois',
'icon-image': iconExpression,
'icon-size': 0.8,
'icon-allow-overlap': true,
'text-field': [
'coalesce',
['get', 'name'],
['get', 'name:en'],
''
],
'text-size': 12,
'text-offset': [0, 1.5],
'text-anchor': 'top',
},
paint: {
'text-color': '#333333',
'text-halo-color': '#FFFFFF',
'text-halo-width': 2,
},
);
}
List<dynamic> _buildIconExpression() {
// Build nested case expressions for all categories
List<dynamic> caseExpr = ['case'];
for (final category in categories) {
// Check if category property exists and has a value
caseExpr.add(['has', category]);
caseExpr.add([
'match',
['get', category],
'restaurant', 'restaurant_icon',
'cafe', 'cafe_icon',
// Add more mappings...
'default_icon'
]);
}
// Fallback
caseExpr.add('default_icon');
return caseExpr;
}
}android {
defaultConfig {
minSdkVersion 21 // MapLibre requires API 21+
}
}Make sure you have:
platform :ios, '12.0' # MapLibre requires iOS 12+
use_frameworks!Add your POI icons to assets/icons/ and update pubspec.yaml:
flutter:
assets:
- assets/icons/import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My POI App',
home: const POIMapPage(),
);
}
}
class POIMapPage extends StatefulWidget {
const POIMapPage({super.key});
@override
State<POIMapPage> createState() => _POIMapPageState();
}
class _POIMapPageState extends State<POIMapPage> {
MapLibreMapController? _mapController;
StyleController? _styleController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('POI Map')),
body: MapLibreMap(
options: MapOptions(
initZoom: 14,
initCenter: Position(9.17, 45.46),
initStyle: 'https://demotiles.maplibre.org/style.json',
),
onMapCreated: (controller) => _mapController = controller,
onStyleLoaded: (controller) {
_styleController = controller;
_setupPOIs();
},
onLongClick: _handlePOIClick,
),
);
}
Future<void> _setupPOIs() async {
// Add your POI setup code here
}
Future<void> _handlePOIClick(Position point) async {
// Add your POI click handling here
}
}For a complete working example, refer to:
/example/lib/poi_demo_page.dart- Full POI implementation/example/lib/widget_layer_interactive_page.dart- Layer interaction examples
- Performance: Load only visible POI icons to reduce memory usage
- Caching: Cache POI tile data for offline support
- Customization: Create custom icon expressions based on your POI categories
- Error Handling: Always wrap map operations in try-catch blocks
- Testing: Test on both iOS and Android - the native expression converter ensures consistent behavior
- Ensure icons are loaded before adding the symbol layer
- Check icon names match between expression and loaded images
- Verify tile server is returning data at the current zoom level
- Verify long press handler is registered
- Check that
queryLayersis returning features - Ensure layer IDs match in filter condition
- Rebuild after changes:
flutter clean && flutter build ios - Check Xcode console for native logs
For issues or questions, refer to the example app in the /example directory or check the GitHub repository.