> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/flutter/maps/llms.txt)

# Migrate to v3

The Mapbox Maps SDK for Flutter v3 adds web support and a more ergonomic API. This guide covers the new features and walks you through the changes you need to make to upgrade your app from v2 to v3.

Your app still imports `package:mapbox_maps_flutter/mapbox_maps_flutter.dart`, the same as in v2.

## Version compatibility

|  | v2 | v3 |
| --- | --- | --- |
| Flutter | 3.27.0 or higher | **3.38.1** or higher |
| Dart | 3.4.4 or higher | **3.10.0** or higher |
| Android minSdk | 21 | 21 |
| iOS | 14 or higher | 14 or higher |
| Web | Not supported | Supported, backed by Mapbox GL JS |

## 1. Update dependencies

Update your app to use v3 of `mapbox_maps_flutter`, and make sure your project meets the [version compatibility requirements](#version-compatibility) above.

v3 is a federated plugin split into several packages, but your app only depends on `mapbox_maps_flutter`. It pulls in the Android and iOS implementation, `mapbox_maps_flutter_mobile`, and the web implementation, `mapbox_maps_flutter_web`, automatically. Both share types from `mapbox_maps_flutter_platform_interface`.

![Diagram of the Maps SDK for Flutter v3 packages. Your Flutter app depends on mapbox\_maps\_flutter, which endorses mapbox\_maps\_flutter\_mobile for Android and iOS and mapbox\_maps\_flutter\_web for web. Both platform packages use shared types from mapbox\_maps\_flutter\_platform\_interface.](https://docs.mapbox.com/assets/ideal-img/maps-flutter-v3-package-layout.4a5d93b.480.png)

Full instructions for access tokens, the dependency, and your first map are in the [Get Started guide](https://docs.mapbox.com/flutter/maps/guides/install/).

> **Related content (guide): [Get Started with Maps SDK for Flutter](https://docs.mapbox.com/flutter/maps/guides/install/)**
> 
> Install v3, configure an access token, and add a map on iOS, Android, and web.

## 2. Explore new features

### 2.1 Web support

Your map code can now run on Flutter web, powered by Mapbox GL JS. The same `MapWidget` and most APIs work on Android, iOS, and web.

### 2.2 `MapWidget` is a `StatelessWidget`

`MapWidget` is now a `StatelessWidget` that passes its configuration to a platform widget, which holds the map state and renders the map. Set where the map starts with `viewport`, and move the camera with a `ViewportController`. [Section 3.2](#initial-camera) shows how to switch from `cameraOptions` to `viewport`.

### 2.3 ViewportController

Use a `ViewportController` to move the camera, for example to fly to a new location. Create the controller, pass it to `MapWidget`, and call `moveTo` whenever the camera should change. It replaces `setStateWithViewportAnimation`. [Section 3.2](#animated-viewport-changes) shows the code before and after.

### 2.4 Gesture events

Pan, zoom, rotate, and pitch gestures each have their own `gestureEvents` stream on `mapboxMap.gestures`. Listen to a stream to get updates while the user moves the map, and cancel the subscription when you no longer need it. These streams replace the scroll and zoom listeners. [Section 3.5](#scroll-and-zoom-listeners) shows how to switch.

On web, you can also listen for camera changes made with the keyboard, such as arrow keys and `+`/`-`, through `mapboxMap.gestures.keyboard.gestureEvents`.

### 2.5 Style APIs on the map

You can now call style methods, such as `addLayer`, directly on `MapboxMap` and `Snapshotter`, without going through `.style`.

```dart
// v2
await mapboxMap.style.addLayer(myLayer);

// v3
await mapboxMap.addLayer(myLayer);
```

[Section 3.9](#replace-deprecated-style) has more examples.

### 2.6 Indoor API

The new experimental indoor API lets you show the floors inside a building on Android, iOS, and web. Indoor maps are part of the Mapbox Standard style, but they are disabled by default. To enable them, set the `showIndoor` configuration property on the `basemap` style import to `true` after the style loads. Indoor floor plans only appear for buildings that have indoor map data.

Once indoor maps are enabled, use `mapboxMap.indoor` to work with floors. Listen to `indoorUpdates` to find out which floors are available and which one is selected. Call `selectFloor` to show a different floor, or pass `null` to leave floor selection.

```dart
await mapboxMap.setStyleImportConfigProperty('basemap', 'showIndoor', true);

final subscription = mapboxMap.indoor.indoorUpdates.listen((state) {
  print('Selected floor: ${state.selectedFloorId}');
  print('Available floors: ${state.floors}');
});

await mapboxMap.indoor.selectFloor(floorId);
```

To configure the built-in floor selector, use `mapboxMap.indoorSelector`.

### 2.7 StyleImage

`StyleImage` is a simpler way to add images, such as custom icons, to the map. Create one from a PNG, JPEG, or WebP file with `StyleImage.bytes`, from raw pixels with `StyleImage.rgba`, or from a Flutter `ui.Image` with `StyleImage.fromImage`. Pass it to `addImage` or `updateImageForSource`, and read an image back with `getImage`.

```dart
await mapboxMap.addImage(
  'icon',
  1.0,
  StyleImage.bytes(pngBytes),
);

final image = await mapboxMap.getImage('icon');
```

`StyleImage` replaces the `MbxImage`-based methods. [Section 3.10](#styleimage-api) shows how to switch.

## 3. Replace deprecated APIs and address breaking changes

Some v2 APIs are deprecated or removed in v3. Deprecated APIs still work for now, but they will be removed in a future release. Removed APIs cause build errors until you replace them. The sections below show what to use instead.

### 3.1 Replace `getAccessToken()` with `accessToken`

`MapboxOptions.getAccessToken()` was removed. Read the token from `MapboxOptions.accessToken` instead. `MapboxOptions.setAccessToken` works the same as before.

```dart
// v2
final token = await MapboxOptions.getAccessToken();

// v3
final token = await MapboxOptions.accessToken;
```

### 3.2 Replace camera setup with `viewport` and `ViewportController`

#### Replace `cameraOptions` with `viewport`

`MapWidget.cameraOptions` was removed. Use `viewport` to set where the map starts.

#### Replace `setStateWithViewportAnimation` with `ViewportController`

Use a `ViewportController` to move the camera instead of `setStateWithViewportAnimation`. Dispose the controller in your widget's `dispose` method.

```dart
// v2
MapWidget(
  cameraOptions: CameraOptions(
    center: Point(coordinates: Position(-80.1263, 25.7845)),
    zoom: 12.0,
  ),
)

setStateWithViewportAnimation(
  () => _viewport = CameraViewportState(center: nyc, zoom: 12),
  transition: FlyViewportTransition(duration: Duration(seconds: 2)),
);

// v3
final _viewportController = ViewportController();

MapWidget(
  viewport: CameraViewportState(
    center: Point(coordinates: Position(-80.1263, 25.7845)),
    zoom: 12.0,
  ),
  viewportController: _viewportController,
)

_viewportController.moveTo(
  CameraViewportState(center: nyc, zoom: 12),
  transition: FlyViewportTransition(duration: Duration(seconds: 2)),
  completion: (finished) => print('done: $finished'),
);

@override
void dispose() {
  _viewportController.dispose();
  super.dispose();
}
```

See the [camera playground example](https://github.com/mapbox/mapbox-maps-flutter/blob/main/mapbox_maps_flutter/example/lib/camera/camera_playground_example.dart) for a full sample.

### 3.3 Remove `null` values from `coordinatesForPixels` input

`coordinatesForPixels` now takes a `List<ScreenCoordinate>` instead of a `List<ScreenCoordinate?>`. Remove any `null` entries before you call it.

### 3.4 Replace tap and long-tap listeners with `addInteraction`

The tap and long-tap listeners on `MapWidget` and `MapboxMap` were removed. Add an interaction in `onMapCreated` instead.

```dart
// v2
MapWidget(
  onTapListener: (context) => print(context.point),
  onLongTapListener: (context) => print(context.point),
)

// v3
void _onMapCreated(MapboxMap mapboxMap) {
  mapboxMap.addInteraction(
    TapInteraction.onMap((context) {
      print('tap at ${context.point}');
    }),
  );
  mapboxMap.addInteraction(
    LongTapInteraction.onMap((context) {
      print('long tap at ${context.point}');
    }),
  );
}
```

To respond to taps on specific map features, such as points of interest, buildings, or your own layers, pass a `FeaturesetDescriptor` to `TapInteraction` or `LongTapInteraction`. See the [standard style interactions example](https://docs.mapbox.com/flutter/maps/examples/standard_interactions/).

`LongTapInteraction` is not supported on web yet.

### 3.5 Replace scroll and zoom listeners with `gestureEvents`

Instead of the scroll and zoom listeners on `MapWidget` and `MapboxMap`, listen to the gesture event streams.

```dart
// v2
MapWidget(
  onScrollListener: (context) => print('scroll: ${context.point}'),
  onZoomListener: (context) => print('zoom: ${context.point}'),
)

mapboxMap.onMapScrollListener = (context) => print('scroll: ${context.point}');
mapboxMap.onMapZoomListener = (context) => print('zoom: ${context.point}');

// v3
void _onMapCreated(MapboxMap mapboxMap) {
  mapboxMap.gestures.pan.gestureEvents.listen((context) {
    print('pan: ${context.point}');
  });
  mapboxMap.gestures.zoom.gestureEvents.listen((context) {
    print('zoom: ${context.point}');
  });
}
```

See the [gestures example](https://github.com/mapbox/mapbox-maps-flutter/blob/main/mapbox_maps_flutter/example/lib/interaction/gestures_example.dart) for pan, zoom, rotate, and pitch, including how to stop listening.

### 3.6 Replace annotation click listeners with `tapEvents`

Annotation click listeners, such as `addOnPointAnnotationClickListener`, were removed. Use `tapEvents` on the annotation manager instead.

```dart
// v2
manager.addOnPointAnnotationClickListener(
  (annotation) => print(annotation.id),
);

// v3
manager.tapEvents(onTap: (annotation) {
  print(annotation.id);
});
```

### 3.7 Replace `PointAnnotation.iconImageCrossFade` with `PointAnnotationManager.setIconImageCrossFade`

The symbol layer `iconImageCrossFade` property is no longer data-driven, so you can't set it on each annotation. Set it once on the `PointAnnotationManager` instead. The value applies to all annotations from that manager.

```dart
// v2
final annotation = await manager.create(
  PointAnnotationOptions(
    geometry: point,
    iconImageCrossFade: 0.5,
  ),
);

// v3
final annotation = await manager.create(
  PointAnnotationOptions(geometry: point),
);
await manager.setIconImageCrossFade(0.5);
```

### 3.8 Replace `setCustomHeaders` with `setCustomHeadersForHost`

`setCustomHeaders` was removed. It sent your headers, such as access tokens, to every server the map loaded data from, including third-party servers. Use `setCustomHeadersForHost` to send headers only to the server you choose.

```dart
// v2
mapboxMap.httpService.setCustomHeaders({
  'Authorization': 'Bearer your_secret_token',
});

// v3
mapboxMap.httpService.setCustomHeadersForHost('tiles.example.com', {
  'Authorization': 'Bearer your_secret_token',
});
```

### 3.9 Replace `MapboxMap.style` and `Snapshotter.style` with direct style methods

Call style methods directly on `MapboxMap` and `Snapshotter`, without `.style`. The method names are the same.

```dart
// v2
await mapboxMap.style.setStyleURI(MapboxStyles.STANDARD);
await mapboxMap.style.addLayer(myLayer);
final layer = await mapboxMap.style.getLayer('my-layer');

await snapshotter.style.setStyleURI(MapboxStyles.LIGHT);

// v3
await mapboxMap.setStyleURI(MapboxStyles.STANDARD);
await mapboxMap.addLayer(myLayer);
final layer = await mapboxMap.getLayer('my-layer');

await snapshotter.setStyleURI(MapboxStyles.LIGHT);
```

### 3.10 Replace `MbxImage` style image methods with `StyleImage`

`StyleImage` makes it easier to pass an image. Use `StyleImage.bytes` for a PNG, JPEG, or WebP file, `StyleImage.rgba` for raw pixels, or `StyleImage.fromImage` for a Flutter `ui.Image`.

```dart
// v2
await mapboxMap.addStyleImage(id, scale, mbxImage);
await mapboxMap.updateStyleImageSourceImage(sourceId, mbxImage);
final image = await mapboxMap.getStyleImage(id);

// v3
await mapboxMap.addImage(id, scale, StyleImage.bytes(pngBytes));
await mapboxMap.updateImageForSource(sourceId, StyleImage.rgba(
  width: width,
  height: height,
  pixels: pixels,
));
final image = await mapboxMap.getImage(id);
```

`getImage` and `updateImageForSource` are not supported on web yet.

### 3.11 Remove `ChangeNotifier` usage on `MapboxMap`

`MapboxMap` can no longer be used as a `Listenable` or `ChangeNotifier`. In v2, it never sent any notifications, so listeners you added never ran. You can safely remove that code.

### 3.12 Replace `RenderedQueryGeometry.value` and `type` with typed geometries

`RenderedQueryGeometry` is now a sealed class with a subclass for each kind of geometry: a point, a box, or a list of points. Instead of checking `type` and decoding `value`, use a `switch` to read the typed data directly. The compiler checks that every case is handled.

```dart
// v2
final geometry = RenderedQueryGeometry.fromScreenCoordinate(point);

if (geometry.type == Type.SCREEN_COORDINATE) {
  final decoded = jsonDecode(geometry.value);
  // ...
}

// v3
final geometry = RenderedQueryGeometry.fromScreenCoordinate(point);

switch (geometry) {
  case ScreenCoordinateRenderedQueryGeometry(:final point):
    print(point);
  case ScreenBoxRenderedQueryGeometry(:final box):
    print(box);
  case ScreenCoordinateListRenderedQueryGeometry(:final points):
    print(points);
}
```

### 3.13 Review the new Android display default

On Android, v3 changes the default platform view mode. The map now uses Hybrid Composition with a `SurfaceView`, which avoids the per-frame copy that a `TextureView` needs. v2 used Virtual Display with a `TextureView`.

To keep the v2 behavior, set `MapWidget.androidHostingMode` to the mode you need and set `MapWidget.textureView` to `true`. A `TextureView` is also required for a transparent map background.

### 3.14 Replace `getDebug` and `setDebug` with `getDebugOptions` and `setDebugOptions`

Pass `setDebugOptions` the full list of debug options you want on. Any option that isn't in the list is turned off.

```dart
// v2
await mapboxMap.setDebug([
  MapDebugOptions(data: MapDebugOptionsData.TILE_BORDERS),
], true);
final options = await mapboxMap.getDebug();

// v3
await mapboxMap.setDebugOptions([
  MapWidgetDebugOptions.tileBorders,
  MapWidgetDebugOptions.camera,
]);
final options = await mapboxMap.getDebugOptions();
```

`getDebugOptions` and `setDebugOptions` are not supported on web yet.

### 3.15 Replace renamed types

Some types have new names. Update any code that refers to them.

| v2 | v3 | Notes |
| --- | --- | --- |
| `StyleLayer` | `Layer` | Subclasses, such as `LineLayer`, are unchanged. |
| `StyleSource` | `Source` | Subclasses, such as `GeoJsonSource`, are unchanged. |
| `Request` | `ResourceRequest` |  |
| `Response` | `ResourceResponse` |  |
| `Error` | `ResponseError` |  |
| `LocationSettings`, `*SettingsInterface` | `*SettingsManager` | For example, `CompassSettingsInterface` is now `CompassSettingsManager`. |
| `MapDebugOptions` / `MapDebugOptionsData` | `MapWidgetDebugOptions` | Not supported on web yet. |

## 4. Review other changes

Common types, such as `CameraOptions`, `CameraState`, `MapOptions`, `ViewportState`, and the annotation, offline, style, and event types, moved to `mapbox_maps_flutter_platform_interface`. You can still import them from `package:mapbox_maps_flutter/mapbox_maps_flutter.dart` as before. You only need a direct dependency on `mapbox_maps_flutter_platform_interface` if you're building a custom platform implementation.

Geometry types, such as `Point`, `Polygon`, `LineString`, and `Feature`, now come directly from the [turf](https://pub.dev/packages/turf) package, instead of SDK subclasses with the same names. You can keep importing them from `mapbox_maps_flutter`.

## 5. Test your app

Test your app thoroughly after making these changes to make sure everything works as expected on each platform you support.

## 6. Conclusion

Following the steps above will help you migrate your app to the latest version of the Mapbox Maps SDK for Flutter. If you run into any issues during the migration process, refer to the [Mapbox Maps SDK for Flutter documentation](https://docs.mapbox.com/flutter/maps/) or reach out to the [Mapbox support team](https://support.mapbox.com/) for help.