maplibre_flutter_gpu 0.2.0 copy "maplibre_flutter_gpu: ^0.2.0" to clipboard
maplibre_flutter_gpu: ^0.2.0 copied to clipboard

MapLibre maps for Flutter, rendered with Flutter GPU.

maplibre_flutter_gpu #

Render MapLibre maps with Flutter GPU without Platform Views. Maps and Flutter Widgets stay smooth and synchronized, with support for custom 3D rendering.

Widgets on the map #

Have you ever wanted to place a Flutter widget on a map as a marker?

With conventional map packages, the widget falls out of sync while the map moves.

Flutter widget synchronization comparison

Flutter and the map use different rendering engines, so their frames cannot stay perfectly synchronized. The effect becomes more noticeable on lower-end devices and can make the map difficult to use.

MapLibre Flutter GPU solves this by letting Flutter render the map and the widgets together.

Everything (Text and Symbols) is a Widget #

Every label and symbol on the map is a Flutter widget. You can also place any widget you like at any geographic coordinate.

Use Flutter to restyle place names and regional labels or bring them to life with animations. Map content and your interface share the same widget system and stay synchronized while the camera moves.

Place 3D objects in map space #

Because the map is rendered with Flutter GPU, you can render custom geometry directly in the map's 3D coordinate space from Dart.

Custom 3D rendering on a Flutter GPU map

New to Flutter GPU? That is fine. Modern LLMs can already help implement a surprisingly wide range of Flutter GPU effects.

Turn a flat map pin into a 3D object positioned in geographic space. Animate cars along roads. If you want to go further, you could even build a flight simulator or an FPS game around a real map.

Supported platforms #

MapLibre Flutter GPU currently supports the following platforms.

  • iOS 15.0 or later
  • Android
  • macOS 14.3 or later
  • Windows x64 and ARM64
  • Linux x64 and ARM64

Web is not supported because Flutter GPU is unavailable there.

Getting started #

Flutter 3.47.0 or later is required.

Add MapLibre Flutter GPU to your project with this command.

flutter pub add maplibre_flutter_gpu

Enable Flutter GPU in your app's ios/Runner/Info.plist and macos/Runner/Info.plist.

<key>FLTEnableFlutterGPU</key>
<true/>

On Windows and Linux, enable Flutter GPU when running the app.

flutter run -d windows --enable-flutter-gpu
flutter run -d linux --enable-flutter-gpu

Flutter 3.47 does not yet support Flutter GPU in Windows or Linux release builds.

Import the package and add a MapLibreMap widget.

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

class MapPage extends StatefulWidget {
  const MapPage({super.key});

  @override
  State<MapPage> createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
  MapLibreMapController? _controller;

  @override
  Widget build(BuildContext context) => Scaffold(
    body: MapLibreMap(
      styleString: MapLibreStyles.openfreemapLiberty,
      initialCameraPosition: const CameraPosition(
        target: LatLng(35.6812, 139.7671),
        zoom: 13,
      ),
      onMapCreated: (controller) {
        _controller = controller;
      },
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () {
        _controller?.animateCamera(
          CameraUpdate.newLatLngZoom(
            const LatLng(35.6586, 139.7454),
            15,
          ),
        );
      },
      child: const Icon(Icons.location_searching),
    ),
  );
}

MapLibreMap constructor #

Map and lifecycle #

Parameter Type Default Description
initialCameraPosition CameraPosition? Style camera Camera used when the map is created. Use cameraPosition or the controller for later changes.
cameraPosition CameraPosition? null Externally controlled camera. The latest input takes precedence over the initial camera and disables local camera mutations.
styleString String MapLibreStyles.demo Style URL, raw style JSON, absolute file path, file URI, or Flutter asset path.
onMapCreated MapCreatedCallback? null Receives the controller after renderer creation. The initial style may still be loading.
onStyleLoadedCallback OnStyleLoadedCallback? null Runs after the active style loads and after every successful style replacement.
onCameraMove OnCameraMoveCallback? null Reports camera changes adopted from native frames.
onFrame OnMapFrameCallback? null Receives immutable camera and viewport metadata when a native frame is adopted for rendering.
onCameraIdle OnCameraIdleCallback? null Runs after camera movement settles. It does not mean all map work has finished.
onMapIdle OnMapIdleCallback? null Runs whenever style, camera, and pending map work have fully settled.
onMapClick OnMapClickCallback? null Reports a tap using logical screen coordinates and geographic coordinates.
onMapLongClick OnMapClickCallback? null Reports the start of a long press using screen and geographic coordinates.

Camera and gestures #

Parameter Type Default Description
cameraTargetBounds CameraTargetBounds No explicit bounds Limits the geographic camera target. Web Mercator projection limits still apply.
cameraConstrainMode CameraConstrainMode? Bounds-dependent Selects none, heightOnly, or screen viewport correction. Explicit modes require a rebuilt native bridge.
minMaxZoomPreference MinMaxZoomPreference 0–25.5 Limits camera zoom.
minMaxTiltPreference MinMaxTiltPreference 0–60° Limits camera tilt in degrees.
rotateGesturesEnabled bool true Enables two-finger rotation.
scrollGesturesEnabled bool true Enables one-finger panning.
zoomGesturesEnabled bool true Enables pinch and scroll-wheel zoom.
tiltGesturesEnabled bool true Enables three-finger vertical tilt.
doubleClickZoomEnabled bool? Follows zoom gestures Enables double-tap zoom independently.
gestureOptions MapGestureOptions const MapGestureOptions() Configures gesture animation, thresholds, and sensitivity.
trackCameraPosition bool false Makes the controller notify its listeners when the camera or viewport size changes.

Map controls #

Parameter Type Default Description
compassEnabled bool true Displays the compass when its builder is available.
compassViewPosition CompassViewPosition? Top right Positions the compass.
compassViewMargins Point<num>? (8, 8) Sets compass insets in logical pixels.
compassBuilder CompassWidgetBuilder? Default compass Builds the compass from its bearing and reset callback.
logoEnabled bool false Displays the MapLibre logo when its builder is available.
logoViewPosition LogoViewPosition? Bottom left Positions the logo.
logoViewMargins Point<num>? (8, 8) Sets logo insets in logical pixels.
logoBuilder WidgetBuilder? Default logo Builds the MapLibre logo.
attributionButtonEnabled bool true Displays the attribution button.
attributionButtonPosition AttributionButtonPosition? Bottom right Positions the attribution button.
attributionButtonMargins Point<num>? (8, 8) Sets attribution-button insets in logical pixels.
onAttributionLinkTap AttributionLinkCallback? null Handles links selected in the default attribution dialog.
attributionButtonBuilder AttributionButtonWidgetBuilder? Default button Builds the attribution button.
attributionDialogBuilder WidgetBuilder? Default dialog Builds the dialog opened by the attribution button.
scaleControlEnabled bool false Displays a scale bar when its value can be calculated.
scaleControlPosition ScaleControlPosition Bottom left Positions the scale bar.
scaleControlUnit ScaleControlUnit Metric Selects metric or imperial distance.
scaleControlMargins Point<num> (8, 8) Sets scale-bar insets in logical pixels.
scaleControlMaxWidth double 80 Sets the maximum scale-bar width in logical pixels.
scaleControlAvoidLogo bool true Moves the scale bar above a logo in the same bottom corner.
scaleControlLogoOffset double 27 Sets the logo avoidance offset in logical pixels.
scaleControlBuilder ScaleControlWidgetBuilder? Default scale bar Builds the scale bar from a ScaleBarValue.

Flutter widgets and custom GPU content #

Parameter Type Default Description
foregroundLoadColor Color? Transparent Value supplied to the loading builder.
userLocation MapUserLocation? null Displays an application-supplied position with optional heading and horizontal accuracy.
userLocationBuilder MapUserLocationWidgetBuilder? Default location marker Builds a location marker from adopted frame geometry. Return null to hide it.
overlayBuilder MapOverlayWidgetBuilder? null Builds interactive Flutter content from adopted frame metadata.
loadingBuilder MapLoadingWidgetBuilder? Default overlay Builds the widget displayed while a style loads.
errorBuilder MapErrorWidgetBuilder? ErrorWidget Builds the replacement widget when map creation fails.
symbolIconBuilder SymbolWidgetBuilder? Style sprite Builds every placed symbol icon as a Flutter widget. Return null to hide one.
symbolTextBuilder SymbolWidgetBuilder? Style text Builds every placed symbol label as a Flutter widget. Return null to hide one.
symbolFadeDuration Duration 150 ms Controls symbol fade-in and fade-out.
symbolCullingPadding EdgeInsets 120 horizontal, 60 vertical Keeps symbols built slightly outside the viewport.
symbolCompositingMode SymbolCompositingMode interleaved Preserves style layer order or uses a faster single-surface overlay.
gpuMapRenderCallback MapLibreGpuRenderCallback? null Records geographic GPU geometry inside the map's 3D sequence with shared depth.
gpuRenderCallback MapLibreGpuRenderCallback? null Records a final Flutter GPU overlay above the map.
gpuRepaint Listenable? null Requests new frames for animated custom GPU content.
gpuOverlayDepthMode MapLibreGpuDepthMode isolated Selects isolated or shared depth for the final GPU overlay.
fillExtrusionRendererBuilder FillExtrusionRendererBuilder? null Customizes 3D building layers. See Customizing 3D buildings.

GPU callbacks receive a MapLibreGpuMapTransform.viewProjectionMatrix with normalized depth from 0 at the near plane to 1 at the far plane, matching the map's shared depth buffer. Use this matrix directly in Flutter GPU vertex shaders without another OpenGL depth conversion. Its X/Y inputs are origin-relative world pixels, and its Z input is altitude in meters.

MapLibreMap creates and owns its MapLibreMapController. Receive the controller through onMapCreated, then use it to move the camera, change the style, manage layers, inspect source metadata, and convert between geographic and screen coordinates. Do not dispose the controller yourself.

Customizing 3D buildings #

fillExtrusionRendererBuilder returns a FillExtrusionRenderer for a fill-extrusion style layer, or null to keep MapLibre's rendering. The map keeps tile loading, depth testing, and pass planning. A renderer can:

  • hide individual buildings with isVisible,
  • shade surfaces with a FillExtrusionMaterial fragment shader, and
  • receive each tile's extruded geometry in didAddTile, for example to build collision shapes or debris effects.

Each FillExtrusionPart is one polygon of a feature. Sources such as OpenMapTiles merge buildings with equal attributes into one multipolygon, so parts address individual buildings. Notify repaint after visibility or the material changes.

class HiddenBuildings extends FillExtrusionRenderer {
  HiddenBuildings(this.hidden) : super(repaint: hidden);

  /// Hidden buildings by feature identifier and polygon index.
  final ValueNotifier<Set<(Object?, int)>> hidden;

  @override
  bool isVisible(FillExtrusionPart part) =>
      !hidden.value.contains((part.featureId, part.polygonIndex));
}

// Create the renderer once, for example in State.initState.
final hidden = ValueNotifier<Set<(Object?, int)>>({});
final buildings = HiddenBuildings(hidden);

MapLibreMap(
  styleString: style,
  fillExtrusionRendererBuilder: (layer) =>
      layer.id == 'building-3d' ? buildings : null,
)

// Later, hide a building and repaint the current frame.
hidden.value = {...hidden.value, (featureId, polygonIndex)};

A material supplies a fragment shader from the application's shader bundle. The map's vertex shader provides MapLibre's lit color, the tile position, the outward surface normal with the height fraction, and the ring distance with its meter scale. Up to eight floats of application data per vertex are available through customAttributes. See FillExtrusionMaterial for the exact inputs.

User location #

Pass a MapUserLocation to display a location dot, a heading arrow, and an accuracy area. The application acquires the location and owns permissions and subscriptions. This package does not start location services or follow the location automatically.

MapLibreMap(
  userLocation: MapUserLocation(
    position: const LatLng(35.6812, 139.7671),
    headingDegrees: 45,
    accuracyMeters: 30,
  ),
  userLocationBuilder: (context, state) => MapUserLocationMarker(
    state: state,
    color: Colors.deepPurple,
    accuracyColor: Colors.deepPurple.withValues(alpha: 0.15),
  ),
)

The style controls example uses geolocator for foreground location updates, permission handling, and an explicit follow control. Panning stops follow while location display continues. Applications can use any location provider, including recorded or simulated data.

Interactive Flutter markers #

Use overlayBuilder or an external Stack with the controller's coordinate conversion methods to place Flutter widgets at geographic coordinates.

overlayBuilder runs when map frame metadata changes.

MapLibreMap(
  onMapCreated: (controller) => _controller = controller,
  overlayBuilder: (context, frame) {
    final position = _controller?.toScreenOffset(markerCoordinate);
    if (position == null) return null;

    return Stack(
      children: [
        Positioned(
          left: position.dx,
          top: position.dy,
          child: const Icon(Icons.location_pin),
        ),
      ],
    );
  },
)

With an external Stack, listen to the controller and rebuild to update position when the camera or viewport size changes.

final position = _controller?.toScreenOffset(markerCoordinate);

Stack(
  fit: StackFit.expand,
  children: [
    MapLibreMap(
      trackCameraPosition: true,
      onMapCreated: (controller) => _controller = controller,
    ),
    if (position != null)
      Positioned(
        left: position.dx,
        top: position.dy,
        child: const Icon(Icons.location_pin),
      ),
  ],
)

See the example app for controller listener setup and cleanup.

Externally controlled cameras #

Pass cameraPosition when another widget or controller owns the camera. Changing the value updates the map without animation. Inputs received while initialization or a frame is pending are coalesced to the latest position. The current value is reapplied after style changes, viewport changes, and app resume.

MapLibreMap(
  cameraPosition: camera,
  cameraConstrainMode: CameraConstrainMode.none,
  styleString: MapLibreStyles.openfreemapLiberty,
  onFrame: (frame) {
    debugPrint('Frame ${frame.sequence}: ${frame.camera}');
  },
)

While cameraPosition is non-null, camera gestures and compass reset are disabled. Controller camera mutations throw StateError. Camera queries, style operations, and map tap callbacks remain available. Setting the property to null restores local camera control at the current position.

CameraConstrainMode.none disables viewport correction. Geographic target bounds, zoom limits, and tilt limits still apply. The native minimum zoom is 0. Leaving the mode null retains the default correction for the configured bounds. An explicit mode throws UnsupportedError when the loaded bridge lacks support.

Style-dependent operations #

For operations that depend on the loaded style, wait for onStyleLoadedCallback.

MapLibreMap(
  onMapCreated: (controller) {
    _controller = controller;
  },
  onStyleLoadedCallback: () async {
    await _controller?.setLayerVisibility('building-3d', true);
  },
)

MapLibreMapController #

The controller is used to change a map after it has been created. Save the instance received by onMapCreated for later use.

Style and layers #

Method Description
setStyle(styleString) Starts loading a style URL, JSON document, file, or Flutter asset.
getStyle() Returns the active style JSON when available.
getLayerIds() Returns the IDs of the loaded style layers.
getSourceIds() Returns the IDs of the loaded style sources.
getSourceAttributions() Returns unique source attribution strings.
setLayerVisibility(layerId, visible) Shows or hides a loaded layer.
getLayerVisibility(layerId) Returns layer visibility, or null when the layer does not exist.
addFillExtrusionLayer(...) Adds a maplibre_gl-compatible fill-extrusion layer.
addHeatmapLayer(...) Adds a heatmap layer from point features in an existing source.
addHillshadeLayer(...) Adds shaded relief from an existing raster-dem source.
addLayer(...) Adds a fill-extrusion, heatmap, or hillshade layer from a LayerProperties object.
setLayerProperties(layerId, properties) Updates the properties of a loaded layer.
removeLayer(layerId) Removes a loaded layer.
setFilter(layerId, filter) Applies a JSON-compatible filter and throws when the layer is missing.
setLayerFilter(layerId, filter) Applies a JSON filter string and reports whether the layer was found.
getFilter(layerId) Returns the parsed filter for a layer.

Camera and coordinates #

Method or property Description
cameraPosition Latest camera position cached by the controller.
isCameraMoving Whether a programmatic camera transition is active.
moveCamera(update) Applies a camera update immediately.
animateCamera(update, duration: ...) Animates to a camera update.
easeCamera(update, duration: ..., interpolation: ...) Applies a camera update with configurable easing.
queryCameraPosition() Refreshes and returns the current camera position.
toScreenLocation(latLng) Converts a geographic coordinate to logical screen pixels.
toScreenLocationBatch(latLngs) Converts multiple geographic coordinates in one call.
toScreenOffset(latLng) Synchronous Offset version of toScreenLocation.
toLatLng(screenLocation) Converts logical screen pixels to a geographic coordinate.
toLatLngOffset(screenLocation) Synchronous Offset version of toLatLng.
getVisibleRegion() Returns the current visible geographic bounds.
getMetersPerPixelAtLatitude(latitude) Returns map resolution at a latitude.
setCameraBounds(...) Animates the camera to fit geographic bounds.
updateContentInsets(...) Updates camera padding in logical pixels.
resetNorth() Resets the camera bearing to north.
getPlacedLabels() Returns the latest native label-placement snapshot.
isMapIdle Whether the native map had no pending work after its last frame. This does not include Flutter fling animation.

The controller belongs to its MapLibreMap. Do not call dispose() yourself and do not use it after the map widget has been removed.

See the example app and the standalone examples for Flutter markers, runtime style controls, heatmap and hillshade layers, and custom Flutter GPU rendering.

For implementation boundaries and validation commands, see the source layout guide.


This package is currently in beta. API and runtime stability are not guaranteed. Android in particular still needs testing across a wider range of devices and environments.

Bug reports, device compatibility results, and other feedback are welcome in GitHub Issues.

This is an independent community project. It is not affiliated with or endorsed by the MapLibre organization.