Filter Markers in Flutter
This tutorial shows how to filter which markers are displayed on the map based on categories, search text, or toggle buttons.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Filter by Category
Toggle different categories of markers on and off. Since a CircleLayer paints every point it contains the same color, each active category gets its own layer in the layers list:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class FilterMarkersScreen extends StatefulWidget {
@override
_FilterMarkersScreenState createState() => _FilterMarkersScreenState();
}
class _FilterMarkersScreenState extends State<FilterMarkersScreen> {
MapController? mapController;
// Active category filters
Set<String> activeFilters = {'restaurant', 'hotel', 'museum', 'park'};
// All places with categories
final List<Map<String, dynamic>> places = [
{'id': '1', 'name': 'Le Bistrot', 'category': 'restaurant', 'lat': 48.858, 'lng': 2.340},
{'id': '2', 'name': 'Café de Flore', 'category': 'restaurant', 'lat': 48.854, 'lng': 2.332},
{'id': '3', 'name': 'Grand Hotel', 'category': 'hotel', 'lat': 48.870, 'lng': 2.330},
{'id': '4', 'name': 'Hotel Paris', 'category': 'hotel', 'lat': 48.862, 'lng': 2.350},
{'id': '5', 'name': 'Louvre', 'category': 'museum', 'lat': 48.861, 'lng': 2.338},
{'id': '6', 'name': 'Musée d\'Orsay', 'category': 'museum', 'lat': 48.860, 'lng': 2.326},
{'id': '7', 'name': 'Luxembourg Gardens', 'category': 'park', 'lat': 48.846, 'lng': 2.337},
{'id': '8', 'name': 'Tuileries Garden', 'category': 'park', 'lat': 48.863, 'lng': 2.327},
];
final Map<String, Color> categoryColors = {
'restaurant': Colors.orange,
'hotel': Colors.blue,
'museum': Colors.purple,
'park': Colors.green,
};
final Map<String, IconData> categoryIcons = {
'restaurant': Icons.restaurant,
'hotel': Icons.hotel,
'museum': Icons.museum,
'park': Icons.park,
};
List<Map<String, dynamic>> get filteredPlaces =>
places.where((p) => activeFilters.contains(p['category'])).toList();
List<Point> _pointsForCategory(String category) => filteredPlaces
.where((p) => p['category'] == category)
.map((p) => Point(coordinates: Position(p['lng'], p['lat'])))
.toList();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Filter Markers')),
body: Column(
children: [
// Filter chips
Container(
padding: EdgeInsets.all(8),
color: Colors.grey[100],
child: Wrap(
spacing: 8,
children: categoryColors.keys.map((category) {
final isActive = activeFilters.contains(category);
return FilterChip(
avatar: Icon(
categoryIcons[category],
size: 18,
color: isActive ? Colors.white : Colors.grey,
),
label: Text(
'${category[0].toUpperCase()}${category.substring(1)}',
),
selected: isActive,
selectedColor: Colors.blue,
labelStyle: TextStyle(
color: isActive ? Colors.white : Colors.black87,
),
onSelected: (selected) {
setState(() {
if (selected) {
activeFilters.add(category);
} else {
activeFilters.remove(category);
}
});
},
);
}).toList(),
),
),
// Count
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
color: Colors.grey[50],
child: Row(
children: [
Text(
'Showing ${filteredPlaces.length} of ${places.length} places',
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
),
Spacer(),
TextButton(
onPressed: () => setState(() =>
activeFilters = {'restaurant', 'hotel', 'museum', 'park'}),
child: Text('Show All'),
),
],
),
),
// Map
Expanded(
child: MapMetricsView(
options: MapOptions(
initCenter: Position(2.340, 48.858), // lng, lat
initZoom: 14,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (controller) => mapController = controller,
layers: [
for (final category in categoryColors.keys)
if (activeFilters.contains(category))
CircleLayer(
points: _pointsForCategory(category),
color: categoryColors[category]!,
radius: 7,
strokeColor: Colors.white,
strokeWidth: 1,
),
],
),
),
],
),
);
}
}Filter by Search Text
Search markers by name in real time. A single MarkerLayer is enough here since there's no per-category coloring — the filtered list is passed straight in as points:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class SearchFilterScreen extends StatefulWidget {
@override
_SearchFilterScreenState createState() => _SearchFilterScreenState();
}
class _SearchFilterScreenState extends State<SearchFilterScreen> {
MapController? mapController;
String searchQuery = '';
final List<Map<String, dynamic>> places = [
{'id': '1', 'name': 'Eiffel Tower', 'lat': 48.8584, 'lng': 2.2945},
{'id': '2', 'name': 'Louvre Museum', 'lat': 48.8606, 'lng': 2.3376},
{'id': '3', 'name': 'Notre-Dame', 'lat': 48.8530, 'lng': 2.3499},
{'id': '4', 'name': 'Sacré-Cœur', 'lat': 48.8867, 'lng': 2.3431},
{'id': '5', 'name': 'Arc de Triomphe', 'lat': 48.8738, 'lng': 2.2950},
{'id': '6', 'name': 'Luxembourg Gardens', 'lat': 48.8462, 'lng': 2.3372},
{'id': '7', 'name': 'Moulin Rouge', 'lat': 48.8841, 'lng': 2.3322},
{'id': '8', 'name': 'Musée d\'Orsay', 'lat': 48.8600, 'lng': 2.3266},
];
List<Map<String, dynamic>> get filteredPlaces => searchQuery.isEmpty
? places
: places
.where((p) =>
p['name'].toLowerCase().contains(searchQuery.toLowerCase()))
.toList();
List<Point> get points => filteredPlaces
.map((place) => Point(coordinates: Position(place['lng'], place['lat'])))
.toList();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search Places')),
body: Column(
children: [
// Search bar
Padding(
padding: EdgeInsets.all(8),
child: TextField(
decoration: InputDecoration(
hintText: 'Search places...',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
contentPadding: EdgeInsets.symmetric(horizontal: 12),
suffixIcon: searchQuery.isNotEmpty
? IconButton(
icon: Icon(Icons.clear),
onPressed: () => setState(() => searchQuery = ''),
)
: null,
),
onChanged: (value) => setState(() => searchQuery = value),
),
),
// Results count
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Text(
'${filteredPlaces.length} results',
style: TextStyle(fontSize: 13, color: Colors.grey),
),
),
// Map
Expanded(
child: MapMetricsView(
options: MapOptions(
initCenter: Position(2.330, 48.860), // lng, lat
initZoom: 13,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (controller) => mapController = controller,
layers: [
MarkerLayer(
points: points,
iconAnchor: IconAnchor.bottom,
),
],
),
),
],
),
);
}
}Next Steps
- Add Clusters — Group filtered markers into clusters
- Popup on Click — Show details when tapping filtered markers
- Markers and Annotations — Basic marker features
Tip: For large datasets, filter with .where() in Dart and pass the resulting List<Point> straight into a MarkerLayer or CircleLayer — MapMetrics diffs the layer's points on each rebuild, so there's no need to manually add/remove annotations.