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.