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
| Capability | Annotations | Style Layers |
|---|---|---|
| API | addSymbol(), addCircle(), addFill(), addLine() | addGeoJsonSource() + addSymbolLayer() etc. |
| Complexity | Low | Medium |
| Max features | ~hundreds | 100,000+ |
| Data-driven styling | ✘ No | ✔ Yes (expressions) |
| Tap callbacks | ✔ Built-in | ● Manual (queryRenderedFeatures) |
| Draggable | ✔ Built-in | ● Via onFeatureDrag |
| Clustering | ✘ No | ✔ Yes |
| Live update | updateSymbol() | setGeoJsonSource() |
| Best for | A few interactive pins | Datasets, 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
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.