Skip to content

Filter Features Within a Layer in Flutter

This tutorial shows how to filter features within a single GeoJSON layer using expressions — showing or hiding features based on their properties without removing and re-adding the layer.

Prerequisites

Before you begin, ensure you have:

Filter by Property Value

Show only features matching a specific property value using layer filters:

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

class FilterWithinLayerScreen extends StatefulWidget {
  @override
  _FilterWithinLayerScreenState createState() =>
      _FilterWithinLayerScreenState();
}

class _FilterWithinLayerScreenState extends State<FilterWithinLayerScreen> {
  MapMetricsController? mapController;
  String selectedType = 'all';

  final List<String> filterOptions = ['all', 'capital', 'city', 'town'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Filter Within Layer')),
      body: Column(
        children: [
          // Filter chips
          Container(
            padding: EdgeInsets.all(12),
            child: Wrap(
              spacing: 8,
              children: filterOptions.map((option) {
                return ChoiceChip(
                  label: Text(option == 'all'
                      ? 'Show All'
                      : option[0].toUpperCase() + option.substring(1)),
                  selected: selectedType == option,
                  onSelected: (selected) {
                    if (selected) {
                      setState(() => selectedType = option);
                      _applyFilter();
                    }
                  },
                );
              }).toList(),
            ),
          ),
          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.0, 8.0),
                zoom: 4.0,
              ),
              onStyleLoaded: () {
                _addCitiesLayer();
              },
            ),
          ),
        ],
      ),
    );
  }

  void _addCitiesLayer() {
    final geoJson = {
      'type': 'FeatureCollection',
      'features': [
        {'type': 'Feature', 'properties': {'name': 'Paris', 'type': 'capital', 'pop': 2161}, 'geometry': {'type': 'Point', 'coordinates': [2.3522, 48.8566]}},
        {'type': 'Feature', 'properties': {'name': 'London', 'type': 'capital', 'pop': 8982}, 'geometry': {'type': 'Point', 'coordinates': [-0.1276, 51.5074]}},
        {'type': 'Feature', 'properties': {'name': 'Berlin', 'type': 'capital', 'pop': 3645}, 'geometry': {'type': 'Point', 'coordinates': [13.405, 52.52]}},
        {'type': 'Feature', 'properties': {'name': 'Rome', 'type': 'capital', 'pop': 2873}, 'geometry': {'type': 'Point', 'coordinates': [12.4964, 41.9028]}},
        {'type': 'Feature', 'properties': {'name': 'Barcelona', 'type': 'city', 'pop': 1621}, 'geometry': {'type': 'Point', 'coordinates': [2.1734, 41.3851]}},
        {'type': 'Feature', 'properties': {'name': 'Munich', 'type': 'city', 'pop': 1472}, 'geometry': {'type': 'Point', 'coordinates': [11.582, 48.1351]}},
        {'type': 'Feature', 'properties': {'name': 'Milan', 'type': 'city', 'pop': 1352}, 'geometry': {'type': 'Point', 'coordinates': [9.19, 45.4642]}},
        {'type': 'Feature', 'properties': {'name': 'Lyon', 'type': 'city', 'pop': 516}, 'geometry': {'type': 'Point', 'coordinates': [4.8357, 45.764]}},
        {'type': 'Feature', 'properties': {'name': 'Bruges', 'type': 'town', 'pop': 118}, 'geometry': {'type': 'Point', 'coordinates': [3.2247, 51.2093]}},
        {'type': 'Feature', 'properties': {'name': 'Salzburg', 'type': 'town', 'pop': 155}, 'geometry': {'type': 'Point', 'coordinates': [13.055, 47.8095]}},
        {'type': 'Feature', 'properties': {'name': 'Siena', 'type': 'town', 'pop': 54}, 'geometry': {'type': 'Point', 'coordinates': [11.3308, 43.3188]}},
      ],
    };

    mapController?.addGeoJsonSource('cities', geoJson);

    mapController?.addCircleLayer(
      'cities-layer',
      'cities',
      circleRadius: 8.0,
      circleColor: '#3b82f6',
      circleStrokeColor: '#ffffff',
      circleStrokeWidth: 2.0,
    );

    mapController?.addSymbolLayer(
      'cities-labels',
      'cities',
      textField: '{name}',
      textSize: 12.0,
      textOffset: [0.0, 1.5],
      textAnchor: 'top',
    );
  }

  void _applyFilter() {
    if (selectedType == 'all') {
      // Remove filter — show all features
      mapController?.setFilter('cities-layer', null);
      mapController?.setFilter('cities-labels', null);
    } else {
      // Apply property filter
      final filter = ['==', ['get', 'type'], selectedType];
      mapController?.setFilter('cities-layer', filter);
      mapController?.setFilter('cities-labels', filter);
    }
  }
}

Filter by Numeric Range

Filter features by a numeric property like population:

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

class NumericFilterScreen extends StatefulWidget {
  @override
  _NumericFilterScreenState createState() => _NumericFilterScreenState();
}

class _NumericFilterScreenState extends State<NumericFilterScreen> {
  MapMetricsController? mapController;
  RangeValues populationRange = RangeValues(0, 10000);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Population Filter')),
      body: Column(
        children: [
          // Range slider
          Container(
            padding: EdgeInsets.all(16),
            child: Column(
              children: [
                Text(
                  'Population: ${populationRange.start.toInt()}K - ${populationRange.end.toInt()}K',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                RangeSlider(
                  values: populationRange,
                  min: 0,
                  max: 10000,
                  divisions: 100,
                  labels: RangeLabels(
                    '${populationRange.start.toInt()}K',
                    '${populationRange.end.toInt()}K',
                  ),
                  onChanged: (values) {
                    setState(() => populationRange = values);
                    _applyPopulationFilter();
                  },
                ),
              ],
            ),
          ),
          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.0, 8.0),
                zoom: 4.0,
              ),
              onStyleLoaded: () {
                _addCitiesWithPopulation();
              },
            ),
          ),
        ],
      ),
    );
  }

  void _addCitiesWithPopulation() {
    final geoJson = {
      'type': 'FeatureCollection',
      'features': [
        {'type': 'Feature', 'properties': {'name': 'London', 'pop': 8982}, 'geometry': {'type': 'Point', 'coordinates': [-0.1276, 51.5074]}},
        {'type': 'Feature', 'properties': {'name': 'Berlin', 'pop': 3645}, 'geometry': {'type': 'Point', 'coordinates': [13.405, 52.52]}},
        {'type': 'Feature', 'properties': {'name': 'Madrid', 'pop': 3223}, 'geometry': {'type': 'Point', 'coordinates': [-3.7038, 40.4168]}},
        {'type': 'Feature', 'properties': {'name': 'Rome', 'pop': 2873}, 'geometry': {'type': 'Point', 'coordinates': [12.4964, 41.9028]}},
        {'type': 'Feature', 'properties': {'name': 'Paris', 'pop': 2161}, 'geometry': {'type': 'Point', 'coordinates': [2.3522, 48.8566]}},
        {'type': 'Feature', 'properties': {'name': 'Vienna', 'pop': 1897}, 'geometry': {'type': 'Point', 'coordinates': [16.3738, 48.2082]}},
        {'type': 'Feature', 'properties': {'name': 'Barcelona', 'pop': 1621}, 'geometry': {'type': 'Point', 'coordinates': [2.1734, 41.3851]}},
        {'type': 'Feature', 'properties': {'name': 'Munich', 'pop': 1472}, 'geometry': {'type': 'Point', 'coordinates': [11.582, 48.1351]}},
        {'type': 'Feature', 'properties': {'name': 'Amsterdam', 'pop': 873}, 'geometry': {'type': 'Point', 'coordinates': [4.9041, 52.3676]}},
        {'type': 'Feature', 'properties': {'name': 'Bruges', 'pop': 118}, 'geometry': {'type': 'Point', 'coordinates': [3.2247, 51.2093]}},
      ],
    };

    mapController?.addGeoJsonSource('pop-cities', geoJson);

    mapController?.addCircleLayer(
      'pop-cities-layer',
      'pop-cities',
      circleRadius: 8.0,
      circleColor: '#ef4444',
      circleStrokeColor: '#ffffff',
      circleStrokeWidth: 2.0,
    );

    mapController?.addSymbolLayer(
      'pop-cities-labels',
      'pop-cities',
      textField: '{name}',
      textSize: 11.0,
      textOffset: [0.0, 1.5],
      textAnchor: 'top',
    );
  }

  void _applyPopulationFilter() {
    final filter = [
      'all',
      ['>=', ['get', 'pop'], populationRange.start.toInt()],
      ['<=', ['get', 'pop'], populationRange.end.toInt()],
    ];
    mapController?.setFilter('pop-cities-layer', filter);
    mapController?.setFilter('pop-cities-labels', filter);
  }
}

Common Filter Expressions

ExpressionDescription
['==', ['get', 'type'], 'capital']Equals
['!=', ['get', 'type'], 'town']Not equals
['>=', ['get', 'pop'], 1000]Greater than or equal
['in', 'capital', ['get', 'type']]Value in list
['all', filter1, filter2]AND (both must match)
['any', filter1, filter2]OR (either matches)
nullRemove filter (show all)

Next Steps


Tip: Layer filters are more performant than removing/re-adding features because the data stays in memory and the map only needs to re-render — not re-parse — the GeoJSON.