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:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Chip-Based Category Filter
Toggle categories on/off with filter chips:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class ToggleFilterScreen extends StatefulWidget {
@override
_ToggleFilterScreenState createState() => _ToggleFilterScreenState();
}
class _ToggleFilterScreenState extends State<ToggleFilterScreen> {
MapMetricsController? 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 Map<String, double> categoryHues = {
'Restaurant': BitmapDescriptor.hueRed,
'Hotel': BitmapDescriptor.hueBlue,
'Museum': BitmapDescriptor.hueViolet,
'Park': BitmapDescriptor.hueGreen,
};
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();
}
@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: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8566, 2.3300),
zoom: 13.0,
),
markers: visiblePlaces.map((place) {
return Marker(
markerId: MarkerId(place['name']),
position: LatLng(place['lat'], place['lng']),
icon: BitmapDescriptor.defaultMarkerWithHue(
categoryHues[place['category']]!,
),
infoWindow: InfoWindow(
title: place['name'],
snippet: place['category'],
),
);
}).toSet(),
),
),
],
),
);
}
}Checkbox Toggle List with Counts
Show categories in a collapsible drawer with checkbox toggles and counts:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CheckboxFilterScreen extends StatefulWidget {
@override
_CheckboxFilterScreenState createState() => _CheckboxFilterScreenState();
}
class _CheckboxFilterScreenState extends State<CheckboxFilterScreen> {
MapMetricsController? 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 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;
}
@override
Widget build(BuildContext context) {
final visible = allPlaces.where((p) => filters[p['category']]!).toList();
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: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.858, 2.335),
zoom: 13.5,
),
markers: visible.map((place) {
return Marker(
markerId: MarkerId(place['name']),
position: LatLng(place['lat'], place['lng']),
infoWindow: InfoWindow(
title: place['name'],
snippet: place['category'],
),
);
}).toSet(),
),
),
],
),
);
}
}Next Steps
- Filter by Text Input — Search-based filtering
- Filter Markers — Simple marker filtering
- Add Custom Icons with Markers — Category-colored markers
Tip: Use FilterChip for mobile-friendly category toggles, and CheckboxListTile for desktop-style sidebar filters. Combine both with setState() to instantly show/hide markers.