Skip to content

Annotations vs Style Layers

This is the most important conceptual distinction in flutter-maplibre-gl. The library provides two completely different APIs for putting things on a map. Choosing the right one saves you from performance problems and unexpected limitations.

The short version

CapabilityAnnotationsStyle Layers
APIaddSymbol(), addCircle(), addFill(), addLine()addGeoJsonSource() + addSymbolLayer() etc.
ComplexityLowMedium
Max features~hundreds100,000+
Data-driven styling No Yes (expressions)
Tap callbacks Built-in Manual (queryRenderedFeatures)
Draggable Built-in Via onFeatureDrag
Clustering No Yes
Live updateupdateSymbol()setGeoJsonSource()
Best forA few interactive pinsDatasets, heatmaps, clusters

Annotations: the simple API

Annotations are Flutter objects that represent individual features. You add them one at a time and get back a typed handle to update or remove later.

// Add a marker
final symbol = await controller.addSymbol(
  const SymbolOptions(
    geometry: LatLng(48.8566, 2.3522),
    iconImage: 'my-pin',
    textField: 'Paris',
  ),
);

// Update it later
await controller.updateSymbol(
  symbol,
  const SymbolOptions(textField: 'Paris, France'),
);

// Remove it
await controller.removeSymbol(symbol);

// React to taps
controller.onSymbolTapped.add((Symbol s) {
  print('Tapped: ${s.options.textField}');
});

Annotation types: Symbol (icons + text), Circle, Fill (polygon), Line

What happens under the hood

AnnotationManager internally creates a hidden GeoJSON source and a style layer for each annotation type. When you call addSymbol(), the manager adds a feature to that source. Annotations are a convenience wrapper over style layers. They trade flexibility for simplicity.

Style Layers: the powerful API

Style layers give you direct access to the MapLibre style specification. You manage the data yourself (as GeoJSON), and MapLibre renders it using data-driven expressions.

// 1. Add the data source
await controller.addGeoJsonSource('cities', {
  'type': 'FeatureCollection',
  'features': [
    {
      'type': 'Feature',
      'properties': {'name': 'Paris', 'population': 2161000},
      'geometry': {'type': 'Point', 'coordinates': [2.3522, 48.8566]},
    },
    // ... thousands more
  ],
});

// 2. Add a layer that renders the source
await controller.addSymbolLayer(
  'cities',           // source id
  'cities-labels',   // layer id
  SymbolLayerProperties(
    textField: [Expressions.get, 'name'],       // read from property
    textSize: [
      Expressions.interpolate, ['linear'],
      [Expressions.get, 'population'],
      100000, 10.0,   // small city → 10px
      5000000, 18.0,  // large city → 18px
    ],
  ),
);

// 3. Update all data at once
await controller.setGeoJsonSource('cities', newFeatureCollection);

What you get that Annotations don't have

  • Data-driven expressions: style any property based on feature data
  • Filters: show/hide features based on properties: filter: ['==', ['get', 'category'], 'park']
  • Clustering: group nearby points automatically at low zoom
  • Heatmaps: density visualization
  • Performance at scale: render hundreds of thousands of features natively

Side-by-side: the same markers, two ways

Annotations

// Simple, managed
await controller.addSymbol(
  const SymbolOptions(
    geometry: LatLng(48.8566, 2.3522),
    iconImage: 'my-pin',
    iconColor: '#E74C3C',
    textField: 'Paris',
  ),
);

// Built-in tap handling
controller.onSymbolTapped.add(
  (s) => print('Tapped!'),
);

Style Layers

// Source + layer
await controller.addGeoJsonSource('pts', {
  'type': 'FeatureCollection',
  'features': [{
    'type': 'Feature',
    'properties': {'name': 'Paris'},
    'geometry': {
      'type': 'Point',
      'coordinates': [2.3522, 48.8566],
    },
  }],
});

await controller.addSymbolLayer('pts', 'pts-layer',
  SymbolLayerProperties(
    iconImage: 'my-pin',
    iconColor: '#E74C3C',
    textField: [Expressions.get, 'name'],
  ),
);

Live demo

Annotations (5 tappable landmarks via addSymbol()):

Style Layers (10 cities via addGeoJsonSource + addSymbolLayer):

Decision guide

Start with Annotations when

  • You have fewer than ~50 features
  • Each feature needs a tap callback
  • Features need to be individually draggable
  • You need quick prototyping

Switch to Style Layers when

  • You have more than ~50 features
  • You need clustering
  • You need data-driven styling (color or size by property)
  • You need heatmaps
  • Performance matters (large datasets)
  • You want to load GeoJSON from a URL

You can mix both

It's valid to use annotations for a few interactive pins and a style layer for a large GeoJSON dataset on the same map. They coexist independently.