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 Features by Toggle List in Flutter

This tutorial shows how to filter map markers using a toggle list of categories — like a checkbox or chip filter panel.

Prerequisites

Before you begin, ensure you have:

Chip-Based Category Filter

Toggle categories on/off with filter chips. Because MarkerLayer/CircleLayer apply one uniform color to every point they render, a per-category color is expressed as one layer per active category rather than a single layer with per-marker colors:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class ToggleFilterScreen extends StatefulWidget {
  @override
  _ToggleFilterScreenState createState() => _ToggleFilterScreenState();
}

class _ToggleFilterScreenState extends State<ToggleFilterScreen> {
  MapController? mapController;

  final Map<String, bool> activeFilters = {
    'Restaurant': true,
    'Hotel': true,
    'Museum': true,
    'Park': true,
  };

  final Map<String, Color> categoryColors = {
    'Restaurant': Colors.red,
    'Hotel': Colors.blue,
    'Museum': Colors.purple,
    'Park': Colors.green,
  };

  final List<Map<String, dynamic>> places = [
    {'name': 'Le Jules Verne', 'category': 'Restaurant', 'lat': 48.8580, 'lng': 2.2945},
    {'name': 'Cafe de Flore', 'category': 'Restaurant', 'lat': 48.8540, 'lng': 2.3326},
    {'name': 'Chez Janou', 'category': 'Restaurant', 'lat': 48.8570, 'lng': 2.3650},
    {'name': 'Hotel Ritz', 'category': 'Hotel', 'lat': 48.8682, 'lng': 2.3285},
    {'name': 'Hotel Lutetia', 'category': 'Hotel', 'lat': 48.8510, 'lng': 2.3268},
    {'name': 'Hotel Plaza', 'category': 'Hotel', 'lat': 48.8716, 'lng': 2.3044},
    {'name': 'Louvre', 'category': 'Museum', 'lat': 48.8606, 'lng': 2.3376},
    {'name': 'Musee d\'Orsay', 'category': 'Museum', 'lat': 48.8600, 'lng': 2.3266},
    {'name': 'Rodin Museum', 'category': 'Museum', 'lat': 48.8554, 'lng': 2.3158},
    {'name': 'Luxembourg Gardens', 'category': 'Park', 'lat': 48.8462, 'lng': 2.3372},
    {'name': 'Tuileries Garden', 'category': 'Park', 'lat': 48.8634, 'lng': 2.3275},
    {'name': 'Champ de Mars', 'category': 'Park', 'lat': 48.8557, 'lng': 2.2986},
  ];

  List<Map<String, dynamic>> get visiblePlaces {
    return places.where((p) => activeFilters[p['category']] == true).toList();
  }

  List<Point> _pointsForCategory(String category) => visiblePlaces
      .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 by Category')),
      body: Column(
        children: [
          // Filter chips
          Container(
            padding: EdgeInsets.all(12),
            color: Colors.white,
            child: Wrap(
              spacing: 8,
              children: activeFilters.keys.map((category) {
                final isActive = activeFilters[category]!;
                final color = categoryColors[category]!;
                return FilterChip(
                  label: Text(category),
                  selected: isActive,
                  selectedColor: color.withOpacity(0.2),
                  checkmarkColor: color,
                  onSelected: (selected) {
                    setState(() {
                      activeFilters[category] = selected;
                    });
                  },
                );
              }).toList(),
            ),
          ),
          // Count bar
          Container(
            padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
            color: Colors.grey[100],
            width: double.infinity,
            child: Text(
              '${visiblePlaces.length} of ${places.length} places shown',
              style: TextStyle(color: Colors.grey[600], fontSize: 12),
            ),
          ),
          // Map
          Expanded(
            child: MapMetricsView(
              options: MapOptions(
                initCenter: Position(2.3300, 48.8566), // lng, lat — Paris
                initZoom: 13,
                initStyle:
                    'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              ),
              onMapCreated: (MapController controller) {
                mapController = controller;
              },
              layers: [
                for (final category in categoryColors.keys)
                  if (activeFilters[category] == true)
                    CircleLayer(
                      points: _pointsForCategory(category),
                      color: categoryColors[category]!,
                      radius: 7,
                      strokeColor: Colors.white,
                      strokeWidth: 1,
                    ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Checkbox Toggle List with Counts

Show categories in a collapsible drawer with checkbox toggles and counts. The map side uses the same one-CircleLayer-per-active-category pattern:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class CheckboxFilterScreen extends StatefulWidget {
  @override
  _CheckboxFilterScreenState createState() => _CheckboxFilterScreenState();
}

class _CheckboxFilterScreenState extends State<CheckboxFilterScreen> {
  MapController? mapController;
  bool showFilters = true;

  final Map<String, bool> filters = {
    'Restaurant': true,
    'Hotel': true,
    'Museum': true,
    'Park': true,
    'Shop': true,
  };

  final Map<String, IconData> categoryIcons = {
    'Restaurant': Icons.restaurant,
    'Hotel': Icons.hotel,
    'Museum': Icons.museum,
    'Park': Icons.park,
    'Shop': Icons.shopping_bag,
  };

  final Map<String, Color> categoryColors = {
    'Restaurant': Colors.red,
    'Hotel': Colors.blue,
    'Museum': Colors.purple,
    'Park': Colors.green,
    'Shop': Colors.orange,
  };

  final List<Map<String, dynamic>> allPlaces = [
    {'name': 'Bistro A', 'category': 'Restaurant', 'lat': 48.858, 'lng': 2.294},
    {'name': 'Bistro B', 'category': 'Restaurant', 'lat': 48.854, 'lng': 2.333},
    {'name': 'Hotel A', 'category': 'Hotel', 'lat': 48.868, 'lng': 2.328},
    {'name': 'Hotel B', 'category': 'Hotel', 'lat': 48.851, 'lng': 2.327},
    {'name': 'Louvre', 'category': 'Museum', 'lat': 48.861, 'lng': 2.338},
    {'name': 'Orsay', 'category': 'Museum', 'lat': 48.860, 'lng': 2.327},
    {'name': 'Pompidou', 'category': 'Museum', 'lat': 48.861, 'lng': 2.352},
    {'name': 'Luxembourg', 'category': 'Park', 'lat': 48.846, 'lng': 2.337},
    {'name': 'Tuileries', 'category': 'Park', 'lat': 48.863, 'lng': 2.328},
    {'name': 'Galeries Lafayette', 'category': 'Shop', 'lat': 48.874, 'lng': 2.332},
    {'name': 'Le Marais Shops', 'category': 'Shop', 'lat': 48.857, 'lng': 2.362},
  ];

  int _countCategory(String category) {
    return allPlaces.where((p) => p['category'] == category).length;
  }

  List<Point> _pointsForCategory(String category) => allPlaces
      .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('Toggle Filter List'),
        actions: [
          IconButton(
            icon: Icon(showFilters ? Icons.filter_list_off : Icons.filter_list),
            onPressed: () => setState(() => showFilters = !showFilters),
          ),
          TextButton(
            onPressed: () {
              setState(() {
                filters.updateAll((key, value) => true);
              });
            },
            child: Text('All', style: TextStyle(color: Colors.white)),
          ),
          TextButton(
            onPressed: () {
              setState(() {
                filters.updateAll((key, value) => false);
              });
            },
            child: Text('None', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
      body: Row(
        children: [
          // Filter panel
          if (showFilters)
            Container(
              width: 180,
              color: Colors.white,
              child: ListView(
                children: filters.keys.map((category) {
                  return CheckboxListTile(
                    value: filters[category],
                    onChanged: (val) {
                      setState(() {
                        filters[category] = val!;
                      });
                    },
                    title: Row(
                      children: [
                        Icon(categoryIcons[category], size: 18),
                        SizedBox(width: 6),
                        Expanded(child: Text(category, style: TextStyle(fontSize: 13))),
                      ],
                    ),
                    subtitle: Text('${_countCategory(category)} places',
                        style: TextStyle(fontSize: 11)),
                    dense: true,
                    controlAffinity: ListTileControlAffinity.leading,
                  );
                }).toList(),
              ),
            ),
          // Map
          Expanded(
            child: MapMetricsView(
              options: MapOptions(
                initCenter: Position(2.335, 48.858), // lng, lat
                initZoom: 13.5,
                initStyle:
                    'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
              ),
              onMapCreated: (MapController controller) {
                mapController = controller;
              },
              layers: [
                for (final category in filters.keys)
                  if (filters[category] == true)
                    CircleLayer(
                      points: _pointsForCategory(category),
                      color: categoryColors[category]!,
                      radius: 7,
                      strokeColor: Colors.white,
                      strokeWidth: 1,
                    ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Next Steps


Tip: Use FilterChip for mobile-friendly category toggles, and CheckboxListTile for desktop-style sidebar filters. Combine both with setState() to instantly rebuild the map's layers list.