Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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:

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:

dart
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:

dart
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 ​


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.