Upgrade from 0.3.x to 0.4
The 0.4 series replaces the MapLibre Android and iOS SDKs and the WebView
desktop integration with one implementation on the
MapLibre Native C API. It
also makes style layers, expressions, sources and map options typed. This
guide applies to the 0.4.0-pre.* pre-releases and to 0.4.0. The
changelog lists every change.
1. Update the dependency
dependencies:
maplibre: ^0.4.0-pre.2
The packages need Dart 3.13 and Flutter 3.47 or newer.
2. Platform setup
maplibre now uses maplibre_web in the browser and maplibre_native on
Android, iOS, macOS, Windows and Linux. maplibre_android, maplibre_ios
and maplibre_webview are discontinued. If you depend on one of them
directly, remove it.
| Platform | Change |
|---|---|
| Web | maplibre-gl-js v6 loads on its own. Remove the maplibre-gl-js and PMTiles <script> and <link> tags from web/index.html. |
| Android | Requires Android 7.0 (API level 24) and OpenGL ES 3.0. Plain http:// URLs need a network security config. |
| iOS | Requires iOS 15. Swift Package Manager only, CocoaPods is not supported. |
| macOS | Requires macOS 14.3. Swift Package Manager only. Works out of the box, maplibre_webview is no longer needed. |
| Windows | Works out of the box, maplibre_webview is no longer needed. |
| Linux | New. Needs the GTK and libepoxy development packages to build. |
See Platform setup for the details.
maplibre-gl-js v6 requires WebGL 2, returns nested objects in GeoJSON
feature properties as maps instead of JSON strings, and sets
zoomLevelsToOverscale to 4 by default, which can change label rendering and
queryRenderedFeatures() results. See the
maplibre-gl-js migration guide.
3. Map options
The style and the initial camera are typed, so they are no longer guessed:
// 0.3
MapOptions(
initStyle: 'https://demotiles.maplibre.org/style.json',
initCenter: Geographic(lon: 9.17, lat: 47.68),
initZoom: 7,
)
// 0.4
MapOptions(
style: MapStyle.url('https://demotiles.maplibre.org/style.json'),
initCamera: CameraUpdate.position(
center: Geographic(lon: 9.17, lat: 47.68),
zoom: 7,
),
)
initStyleis nowstyle, aMapStyle.url,MapStyle.json,MapStyle.assetorMapStyle.file. The map loads a new style whenstylechanges, soMapController.setStylewas removed.initCenter,initZoom,initBearingandinitPitchare nowinitCamera, aCameraUpdatesuch asCameraUpdate.positionorCameraUpdate.bounds, see Camera. WithoutinitCamera, the map starts at the camera of the style instead of zoom 0.androidMode,androidTextureMode,androidTranslucentTextureSurface,androidForegroundLoadColor,webviewDebugModeand the deprecatedpitchwere removed.
What the user may change moved from MapGestures to MapMovements in
MapOptions.movements, and MapGestures now turns single gestures on and
off:
| 0.3 | 0.4 |
|---|---|
gestures: MapGestures.all() |
nothing, the default |
gestures: MapGestures.all(pitch: false) |
movements: MapMovements(pitch: false) |
gestures: MapGestures.none(zoom: true) |
movements: MapMovements.none(zoom: true) |
gestures: MapGestures.none() |
movements: MapMovements.none() |
gestures.allEnabled |
movements == const MapMovements() |
See Gestures.
4. Style layers and expressions
Style layers take typed properties and
expressions instead of paint and layout
maps. The annotation layers take expressions too.
// 0.3
LineStyleLayer(
id: 'route',
sourceId: 'route',
paint: {'line-color': '#FF0000', 'line-width': 3},
layout: {'line-cap': 'round'},
)
// 0.4
LineStyleLayer(
id: 'route',
sourceId: 'route',
color: const .literal(Color(0xFFFF0000)),
width: const .literal(3),
cap: const .literal(LineCap.round),
)
Expression.raw takes the JSON of a single property, and
StyleLayer.fromJson the JSON of a whole layer.
5. Sources and layers of the style
style.layers and style.sources replace the layer and source methods of
StyleController:
| 0.3 | 0.4 |
|---|---|
style.addLayer(layer) |
style.layers.add(layer) |
style.removeLayer(id) |
style.layers.remove(id) |
style.addSource(source) |
style.sources.add(source) |
style.removeSource(id) |
style.sources.remove(id) |
style.updateGeoJsonSource(...) |
style.sources.updateGeoJsonData(...) or updateGeoJsonUrl(...) |
style.getLayerIds() |
style.layers.map((layer) => layer.id) |
style.getAttributions() |
style.attributions |
Sources are sealed classes with named constructors:
| 0.3 | 0.4 |
|---|---|
GeoJsonSource(data: url) |
GeoJsonSource.url(url: url) |
GeoJsonSource(data: geoJson) |
GeoJsonSource.data(data: .text(geoJson)) |
ImageSource(url: url) |
ImageSource.url(url: url), or .bytes / .provider |
VectorSource(url: tileJsonUrl) |
VectorSource.url(url: tileJsonUrl) |
VectorSource(tiles: [...]) |
VectorSource.tiles(tiles: [...]) |
RasterSource, RasterDemSource |
.url and .tiles, like VectorSource |
VideoSource(urls: [...]) |
VideoSource.urls(urls: [...]) |
VideoSource.coordinates is a LngLatQuad, like ImageSource.coordinates.
VideoSource is only supported on web.
GeoJSON data is a GeoJsonData instead of a string, in GeoJsonSource.data
and updateGeoJsonData. Wrap GeoJSON text in .text(...), decoded JSON in
.json(...), or pass geobase objects without encoding them yourself:
// 0.3
GeoJsonSource(id: 'points', data: jsonEncode(geoJson));
await style.updateGeoJsonSource(id: 'points', data: collection.toText());
// 0.4
GeoJsonSource.data(id: 'points', data: .json(geoJson));
await style.sources.updateGeoJsonData('points', .featureCollection(collection));
GeoJsonSourceData.data returns that GeoJsonData, whose toText() and
toJson() give the GeoJSON.
6. Images
addImage, addImages and addImageFromAssets take a pixelRatio of image
pixels per logical pixel, 1 by default, instead of the device pixel ratio.
Images now show at the same logical size on every screen, so they grow on
high-DPI screens. Pass the ratio the image was made for to keep their size:
await style.addImage('marker', bytes, pixelRatio: 2);
addImageFromCanvas and addImageFromIconData take their sizes in logical
pixels.
Every image method takes StyleImageOptions for SDF images (sdf) and images
that fit the text of a symbol (stretchX, stretchY, content,
textFitWidth, textFitHeight).
7. Geometry types
LngLatBoundsis replaced by geobase'sGeoBoxinfitBounds,getVisibleRegion,MapOptions.maxBoundsand offline regions.GeoBox.from(points)replacesLngLatBounds.fromPoints(points).maplibreexports only the geobase types its API uses. Importpackage:geobase/geobase.dartfor the others.
8. Feature queries
featuresAtPoint and featuresInRect are now one queryRenderedFeatures,
which returns a Future, so await it:
// 0.3
final features = controller.featuresAtPoint(point);
final inRect = controller.featuresInRect(rect);
// 0.4
final features = await controller.queryRenderedFeatures(.point(point));
final inRect = await controller.queryRenderedFeatures(.rect(rect));
QueryGeometry.viewport()queries the whole map, andfiltertakes anExpression<bool>, like a layer filter.RenderedFeature.geometryis a geobaseGeometryinstead of a GeoJSON map, for example aPointor aPolygon.queryLayersandQueriedLayerwere removed. UsequeryRenderedFeatureswithlayerIds, andRenderedFeature.layerId,sourceIdandsourceLayer.
9. User location and permissions
PermissionManager, MapController.enableLocation, trackLocation,
userLocationIsSupported, BearingRenderMode and BearingTrackMode were
removed. Show the location with a UserLocationLayer
and ask for permissions with geolocator or permission_handler.
MapControlButtons takes tracking and onTrackingPressed instead of
showTrackLocation.
10. Offline regions
OfflineManager.downloadRegion takes an OfflineRegionDefinition:
// 0.3
manager.downloadRegion(
mapStyleUrl: styleUrl,
bounds: bounds,
minZoom: 10,
maxZoom: 14,
pixelDensity: 1,
);
// 0.4
manager.downloadRegion(
definition: OfflineTilePyramidRegionDefinition(
styleUrl: styleUrl,
bounds: bounds,
minZoom: 10,
maxZoom: 14,
pixelRatio: 1,
),
);
OfflineRegion holds the definition, DownloadProgress holds an
OfflineRegionStatus, and download errors are an OfflineResourceException
or an OfflineTileCountLimitException. setOfflineTileCountLimit and
runPackDatabaseAutomatically were removed. See
Offline & Cache.
11. Web gestures
Map gestures are recognized in Flutter on every platform, also on web.
MapLibre GL JS's keyboard navigation and Shift + drag box zoom are no longer
available. Widgets over the map no longer need a PointerInterceptor.
12. Native platforms
setProjection takes an Expression<MapProjection>, as the style spec does,
so a projection can change with the zoom level. MapProjection adds
verticalPerspective:
// 0.3
style.setProjection(MapProjection.globe);
// 0.4
style.setProjection(const .literal(.globe));
Projections other than Mercator are only supported on web. On every other platform, the map keeps Mercator.
13. Events
onEvent is replaced by one callback per event, and each callback gets its
own event type:
// 0.3
MapLibreMap(
onEvent: (event) {
switch (event) {
case MapEventClick():
print(event.point);
case MapEventCameraIdle():
print('camera idle');
default:
break;
}
},
)
// 0.4
MapLibreMap(
onClick: (event) => print(event.point),
onCameraMoveEnd: (event) => print('camera idle at ${event.camera}'),
)
| 0.3 | 0.4 |
|---|---|
MapEventMapCreated |
onMapCreated |
MapEventStyleLoaded |
onStyleLoaded |
MapEventClick, MapEventDoubleClick, MapEventSecondaryClick, MapEventLongClick |
onClick, onDoubleClick, onSecondaryClick, onLongClick |
MapEventStartMoveCamera |
onCameraMoveStart |
MapEventMoveCamera |
onCameraMove, which also carries the reason |
MapEventCameraIdle |
onCameraMoveEnd, with the camera and the reason |
MapEventIdle |
onIdle |
MapEventRenderingChanged |
onRenderingChanged |
CameraChangeReason.apiGesture, developerAnimation and apiAnimation are
now gesture, programmatic and map. See Events for the
new events.
14. Camera
moveCamera and animateCamera take a CameraUpdate, the same type as
MapOptions.initCamera. fitBounds is now CameraUpdate.bounds, and the
platform-prefixed animation parameters are a CameraAnimation:
// 0.3
await controller.moveCamera(zoom: 10);
await controller.animateCamera(
center: Geographic(lon: 9.17, lat: 47.68),
zoom: 12,
nativeDuration: const Duration(seconds: 1),
webSpeed: 1.5,
);
await controller.fitBounds(bounds: bounds, padding: const EdgeInsets.all(32));
// 0.4
await controller.moveCamera(const CameraUpdate.position(zoom: 10));
await controller.animateCamera(
const CameraUpdate.position(center: Geographic(lon: 9.17, lat: 47.68), zoom: 12),
animation: const CameraAnimation.fly(duration: Duration(seconds: 1)),
);
await controller.animateCamera(
CameraUpdate.bounds(bounds, padding: const EdgeInsets.all(32)),
);
| 0.3 | 0.4 |
|---|---|
InitialCamera.position, .bounds |
CameraUpdate.position, .bounds |
fitBounds(bounds: ...) |
animateCamera(CameraUpdate.bounds(...)) |
nativeDuration, webMaxDuration |
CameraAnimation.ease(duration:) or .fly(duration:) |
webSpeed |
CameraAnimation.fly(speed:) |
webLinear: true |
CameraAnimation.ease() |
webMaxZoom |
maxZoom of CameraUpdate.bounds |
fitBounds(offset:) |
Removed; uneven padding of CameraUpdate.bounds moves the fitted area instead |
getVisibleRegion() |
getVisibleBounds() |
MapCamera, MapCamera.of(context) |
CameraPosition, CameraPosition.of(context) |
MapCompass.nativeRotationDuration, webRotationSpeed |
MapCompass.animation |
A PlatformException when an animation is cancelled (web) |
CameraAnimationResult.cancelled |
animateCamera now eases for 300 ms by default, where 0.3 flew for 2 seconds
on every platform but web. Pass animation: const CameraAnimation.fly() to fly.