
Mapbox Flutter Patterns
- 368 installs
- 71 repo stars
- Updated August 4, 2026
- mapbox/mapbox-agent-skills
Integrate Mapbox maps, layers, camera, and location flows in Flutter apps using recommended SDK patterns and performance-safe defaults.
About
Teaches Mapbox-recommended Flutter integration patterns for interactive maps, geolocation, layers, and camera behavior so mobile apps ship reliable mapping features without SDK anti-patterns.
- Mapbox Flutter SDK setup
- Map layers and camera control
- Location permissions and tracking
- Performance and offline considerations
- Platform-specific Mapbox patterns
Mapbox Flutter Patterns by the numbers
- 368 all-time installs (skills.sh)
- +46 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #344 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-flutter-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 368 |
|---|---|
| repo stars | ★ 71 |
| Last updated | August 4, 2026 |
| Repository | mapbox/mapbox-agent-skills ↗ |
What it does
Integrate Mapbox maps, layers, camera, and location flows in Flutter apps using recommended SDK patterns and performance-safe defaults.
Files
Mapbox Flutter Integration Patterns
Official patterns for integrating the Mapbox Maps SDK for Flutter (mapbox_maps_flutter) on iOS and Android with Dart.
Use this skill when:
- Installing and configuring mapbox_maps_flutter in a Flutter app
- Setting the Mapbox access token the right way
- Initializing a
MapWidgetwith camera / style options - Adding annotations (points, circles, lines, polygons) and handling taps
- Showing the user location puck
- Loading GeoJSON from app assets
- Troubleshooting iOS build failures after adding Mapbox
Official Resources:
Web and desktop are not supported — the Flutter SDK targets iOS and Android only.
---
Installation & Setup
Requirements
- Flutter SDK 3.22.3 / Dart 3.4.4+
- iOS: deployment target 14.0 or higher
- Android: minSdk 21 or higher
- Free Mapbox account
Step 1: Add the dependency
# pubspec.yaml
dependencies:
mapbox_maps_flutter: ^2.0.0flutter pub getStep 2: Bump the iOS deployment target to 14.0 (required)
This is the single most common cause of iOS build failures after adding Mapbox. The Flutter SDK requires iOS 14.0 and will not compile on the Flutter default.
1. Open ios/Runner.xcworkspace in Xcode. 2. Select the Runner target → General → set Minimum Deployments → iOS to 14.0. 3. If ios/Podfile exists, update the platform line too:
# ios/Podfile
platform :ios, '14.0'You do not need to worry about CocoaPods vs Swift Package Manager — mapbox_maps_flutter supports both and Flutter picks whichever your app is configured for.
Step 3: iOS location permission
Add the purpose string to ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Show your location on the map</string>Step 4: Android permissions
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />Step 5: Configure the access token
The recommended pattern is to pass the token via --dart-define at build/run time and set it on MapboxOptions before creating any MapWidget.
flutter run --dart-define=ACCESS_TOKEN=pk.your_token_here// main.dart
import 'package:flutter/material.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
const accessToken = String.fromEnvironment('ACCESS_TOKEN');
void main() {
MapboxOptions.setAccessToken(accessToken);
runApp(const MaterialApp(home: MapScreen()));
}Never hard-code tokens in source. For CI, pass --dart-define=ACCESS_TOKEN=$MAPBOX_ACCESS_TOKEN.
---
Map Initialization
Basic map
import 'package:flutter/material.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
class MapScreen extends StatelessWidget {
const MapScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: MapWidget(
key: const ValueKey('mapWidget'),
cameraOptions: CameraOptions(
center: Point(coordinates: Position(-122.4194, 37.7749)),
zoom: 12,
),
styleUri: MapboxStyles.STANDARD,
),
);
}
}Grab the MapboxMap controller
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@override
State<MapScreen> createState() => _MapScreenState();
}
class _MapScreenState extends State<MapScreen> {
MapboxMap? mapboxMap;
void _onMapCreated(MapboxMap controller) {
mapboxMap = controller;
}
@override
Widget build(BuildContext context) {
return MapWidget(
key: const ValueKey('mapWidget'),
onMapCreated: _onMapCreated,
cameraOptions: CameraOptions(
center: Point(coordinates: Position(-122.4194, 37.7749)),
zoom: 12,
),
);
}
}---
Add Annotations
Use mapboxMap.annotations to create managers for point, circle, polyline, and polygon annotations. Managers are long-lived — create them once and reuse for updates.
Point annotations with a custom image
import 'package:flutter/services.dart' show rootBundle;
PointAnnotationManager? pointAnnotationManager;
Future<void> _addMarkers(MapboxMap mapboxMap) async {
pointAnnotationManager = await mapboxMap.annotations.createPointAnnotationManager();
final bytes = await rootBundle.load('assets/marker.png');
final imageBytes = bytes.buffer.asUint8List();
final options = <PointAnnotationOptions>[
PointAnnotationOptions(
geometry: Point(coordinates: Position(-122.4194, 37.7749)),
image: imageBytes,
iconSize: 1.2,
),
PointAnnotationOptions(
geometry: Point(coordinates: Position(-122.4094, 37.7849)),
image: imageBytes,
),
];
await pointAnnotationManager!.createMulti(options);
}Remember to register the asset in pubspec.yaml:
flutter:
assets:
- assets/marker.pngTap handling
Use manager.tapEvents — this is the current API. addOnPointAnnotationClickListener is deprecated.
tapEvents returns a Cancelable that you store and invoke .cancel() on when the listener is no longer needed:
final Cancelable tapSubscription = pointAnnotationManager!.tapEvents(
onTap: (annotation) {
debugPrint('Tapped annotation ${annotation.id}');
},
);
@override
void dispose() {
tapSubscription.cancel();
super.dispose();
}The same pattern — returning a Cancelable — exists on every manager's longPressEvents and dragEvents, and across the other annotation types (CircleAnnotationManager.tapEvents, etc.).
Load annotations from GeoJSON
import 'dart:convert';
import 'package:flutter/services.dart' show rootBundle;
Future<void> _loadGeoJson(MapboxMap mapboxMap) async {
final raw = await rootBundle.loadString('assets/coffee_shops.geojson');
final geo = jsonDecode(raw) as Map<String, dynamic>;
final features = (geo['features'] as List).cast<Map<String, dynamic>>();
final manager = await mapboxMap.annotations.createPointAnnotationManager();
final icon = (await rootBundle.load('assets/coffee.png')).buffer.asUint8List();
final options = features.map((feature) {
final coords = feature['geometry']['coordinates'] as List;
return PointAnnotationOptions(
geometry: Point(coordinates: Position(coords[0] as double, coords[1] as double)),
image: icon,
);
}).toList();
await manager.createMulti(options);
}For thousands of features use a style layer (GeoJsonSource + SymbolLayer) instead of annotations.
---
Show User Location
Permissions must already be granted (use permission_handler or similar) before enabling the puck.
await mapboxMap.location.updateSettings(LocationComponentSettings(
enabled: true,
puckBearingEnabled: true,
locationPuck: LocationPuck(
locationPuck2D: DefaultLocationPuck2D(),
),
));---
Camera Control
// Instant jump
await mapboxMap.setCamera(CameraOptions(
center: Point(coordinates: Position(-80.1263, 25.7845)),
zoom: 14,
));
// Animated fly-to
await mapboxMap.flyTo(
CameraOptions(
center: Point(coordinates: Position(-80.1263, 25.7845)),
zoom: 17,
bearing: 180,
pitch: 30,
),
MapAnimationOptions(duration: 2000),
);---
Troubleshooting
iOS build fails with "platform is lower than deployment target"
The Flutter default iOS deployment target is lower than Mapbox's minimum (iOS 14). Set Minimum Deployments → iOS to 14.0 on the Runner target in Xcode. If the project has an ios/Podfile, also set platform :ios, '14.0' there and re-run pod install.
setAccessToken not called
If you forget to call MapboxOptions.setAccessToken before creating a MapWidget, the map will load with a blank grid. Always call it in main() before runApp.
Annotation tap handler not firing
Make sure you're using manager.tapEvents(onTap: ...) — addOnPointAnnotationClickListener is deprecated. Also confirm the MapboxMap controller is captured via onMapCreated before you create the annotation manager.
Hot reload after permissions change
iOS/Android will not re-read manifests or Info.plist on hot reload. Fully restart the app after editing permissions.
---
Reference Files
- `references/annotations.md` — Circle, Polyline, Polygon patterns and GeoJSON source/layer recipes.
- `references/platform-setup.md` — Deeper iOS/Android setup, token strategies, release signing notes.
---
Additional Resources
Mapbox Flutter Quick Reference
Fast reference for mapbox_maps_flutter on iOS + Android.
Install
# pubspec.yaml
dependencies:
mapbox_maps_flutter: ^2.0.0- iOS: Runner target Minimum Deployments → iOS = 14.0 (Podfile
platform :ios, '14.0'if using Pods). - Android:
minSdk = 21inandroid/app/build.gradle(.kts).
Access Token
flutter run --dart-define=ACCESS_TOKEN=pk.your_tokenconst accessToken = String.fromEnvironment('ACCESS_TOKEN');
void main() {
MapboxOptions.setAccessToken(accessToken);
runApp(const MaterialApp(home: MapScreen()));
}Basic MapWidget
MapWidget(
key: const ValueKey('mapWidget'),
onMapCreated: (controller) => mapboxMap = controller,
cameraOptions: CameraOptions(
center: Point(coordinates: Position(-122.4194, 37.7749)),
zoom: 12,
),
styleUri: MapboxStyles.STANDARD,
)Point Annotations
final manager = await mapboxMap.annotations.createPointAnnotationManager();
final icon = (await rootBundle.load('assets/marker.png')).buffer.asUint8List();
await manager.createMulti([
PointAnnotationOptions(
geometry: Point(coordinates: Position(-122.4194, 37.7749)),
image: icon,
),
]);
// Current non-deprecated tap API
manager.tapEvents(onTap: (annotation) {
debugPrint('tapped ${annotation.id}');
});
// Also: longPressEvents, dragEventsLoad GeoJSON
final raw = await rootBundle.loadString('assets/data.geojson');
final features = (jsonDecode(raw)['features'] as List).cast<Map<String, dynamic>>();For thousands of features use a GeoJsonSource + SymbolLayer instead of annotations.
User Location
await mapboxMap.location.updateSettings(LocationComponentSettings(
enabled: true,
puckBearingEnabled: true,
locationPuck: LocationPuck(locationPuck2D: DefaultLocationPuck2D()),
));Grant permission first (use permission_handler).
Camera
await mapboxMap.flyTo(
CameraOptions(
center: Point(coordinates: Position(-80.1263, 25.7845)),
zoom: 17,
),
MapAnimationOptions(duration: 2000),
);Checklist
- ✅ iOS Runner target Minimum Deployment = 14.0
- ✅ Android minSdk = 21
- ✅ Location permissions declared in
Info.plist/AndroidManifest.xml - ✅
MapboxOptions.setAccessToken(...)called inmain()beforerunApp - ✅ Annotation managers reused;
manager.tapEvents(not deprecatedaddOn...ClickListener)
Resources
{
"skill_name": "mapbox-flutter-patterns",
"evals": [
{
"id": 1,
"prompt": "I'm adding Mapbox to an existing Flutter app. What are the minimum platform requirements I need to set on iOS and Android before the build succeeds?",
"expectations": [
"iOS deployment target must be bumped to 14.0 (Runner target in Xcode, and the Podfile platform line if a Podfile exists)",
"Identifies the iOS deployment target as the single most common iOS build failure cause",
"Android minSdk must be 21 or higher",
"Does NOT insist CocoaPods is required — mentions that the plugin supports both CocoaPods and SPM and Flutter chooses automatically (or simply does not force one)",
"Does not suggest hard-coding the iOS version only in pbxproj without updating the Runner target in Xcode"
]
},
{
"id": 2,
"prompt": "How should I configure the Mapbox access token in my Flutter app without checking it into the repo?",
"expectations": [
"Pass the token via `--dart-define=ACCESS_TOKEN=pk.your_token` at `flutter run` / `flutter build` time",
"Read it in Dart with `const accessToken = String.fromEnvironment('ACCESS_TOKEN');`",
"Call `MapboxOptions.setAccessToken(accessToken)` in `main()` before `runApp`, so the token is set before any `MapWidget` is created",
"Does NOT suggest hard-coding the token in source code or putting it in pubspec.yaml",
"For CI, suggests reading the token from an environment variable and forwarding it via --dart-define"
]
},
{
"id": 3,
"prompt": "I have a `PointAnnotationManager` in my Flutter app and I want my annotations to be tappable. What's the current non-deprecated API?",
"expectations": [
"Use `manager.tapEvents(onTap: (annotation) { ... })` — returns a `Cancelable`",
"Explicitly calls out that `addOnPointAnnotationClickListener` is deprecated and should not be used",
"Stores the returned Cancelable and calls `.cancel()` when the listener is no longer needed (e.g. in `dispose`)",
"Mentions that the same `tapEvents` / `longPressEvents` / `dragEvents` pattern is available on every annotation manager type"
]
},
{
"id": 4,
"prompt": "I have a `coffee_shops.geojson` file in my Flutter app assets. How do I display each feature as a point annotation on a Mapbox map?",
"expectations": [
"Load the file contents with `rootBundle.loadString('assets/coffee_shops.geojson')`",
"Register the asset path under `flutter: assets:` in pubspec.yaml",
"Decode JSON and iterate `features`, reading each geometry's `coordinates` as `[lng, lat]`",
"Create a single `PointAnnotationManager` via `mapboxMap.annotations.createPointAnnotationManager()` and call `createMulti(options)` — not one call per feature",
"Wraps coordinates in `Point(coordinates: Position(lng, lat))`",
"Mentions that for thousands of features, a `GeoJsonSource` + `SymbolLayer` is more appropriate than annotations"
]
},
{
"id": 5,
"prompt": "My Flutter Mapbox map shows a blank grid and no tiles load. I pass --dart-define=ACCESS_TOKEN=... but nothing renders. What's likely wrong?",
"expectations": [
"Most likely cause: `MapboxOptions.setAccessToken(...)` is never called, or is called after the `MapWidget` is already built",
"Verify the token is read with `String.fromEnvironment('ACCESS_TOKEN')` using the exact same key passed to --dart-define",
"Call `MapboxOptions.setAccessToken(...)` in `main()` before `runApp`",
"Confirm the token is a public token (starts with `pk.`) and has not been revoked",
"Does NOT suggest the deployment target is the issue — that would be a compile-time failure, not a runtime blank map"
]
}
]
}
Annotations and Sources (Flutter)
Deeper reference for annotation managers beyond point annotations, plus the GeoJSON source + style layer approach for large datasets.
---
Circle Annotations
final manager = await mapboxMap.annotations.createCircleAnnotationManager();
await manager.create(CircleAnnotationOptions(
geometry: Point(coordinates: Position(-122.4194, 37.7749)),
circleRadius: 10,
circleColor: Colors.red.toARGB32(),
));
manager.tapEvents(onTap: (annotation) {
debugPrint('circle tapped: ${annotation.id}');
});Polyline Annotations
final manager = await mapboxMap.annotations.createPolylineAnnotationManager();
await manager.create(PolylineAnnotationOptions(
geometry: LineString(coordinates: [
Position(-122.4194, 37.7749),
Position(-122.4094, 37.7849),
Position(-122.3994, 37.7949),
]),
lineColor: Colors.blue.toARGB32(),
lineWidth: 4,
));Polygon Annotations
final manager = await mapboxMap.annotations.createPolygonAnnotationManager();
await manager.create(PolygonAnnotationOptions(
geometry: Polygon(coordinates: [
[
Position(-122.420, 37.770),
Position(-122.410, 37.770),
Position(-122.410, 37.780),
Position(-122.420, 37.780),
Position(-122.420, 37.770),
],
]),
fillColor: Colors.green.toARGB32(),
fillOpacity: 0.4,
));---
GeoJSON Source + Style Layer (large datasets)
Annotations render each feature through a manager which is convenient but expensive. For hundreds or thousands of features, clustering, or data-driven styling, load the GeoJSON into a GeoJsonSource and render it with a SymbolLayer (or CircleLayer / LineLayer / FillLayer).
import 'package:flutter/services.dart' show rootBundle;
Future<void> _addGeoJsonLayer(MapboxMap mapboxMap) async {
final data = await rootBundle.loadString('assets/coffee_shops.geojson');
await mapboxMap.style.addSource(GeoJsonSource(id: 'shops', data: data));
// Register the icon once in the style.
final iconBytes = (await rootBundle.load('assets/coffee.png')).buffer.asUint8List();
await mapboxMap.style.addStyleImage(
'coffee-icon',
1.0,
MbxImage(width: 48, height: 48, data: iconBytes),
false,
[],
[],
null,
);
await mapboxMap.style.addLayer(SymbolLayer(
id: 'shops-layer',
sourceId: 'shops',
iconImage: 'coffee-icon',
iconAllowOverlap: true,
));
}Clustering
Enable clustering on the source and render cluster bubbles with a CircleLayer + count labels with a SymbolLayer filtered by ["has", "point_count"].
await mapboxMap.style.addSource(GeoJsonSource(
id: 'shops',
data: data,
cluster: true,
clusterRadius: 50,
clusterMaxZoom: 14,
));---
Removing annotations
- Per-annotation:
manager.delete(annotation)ormanager.deleteMulti([annotation]). - Clear one manager:
manager.deleteAll(). - Remove the manager and its backing layer/source:
mapboxMap.annotations.removeAnnotationManager(manager).
Platform Setup: iOS, Android, Tokens
Deeper reference for getting mapbox_maps_flutter building and running cleanly on both platforms. The SKILL.md covers the 90% path — come here for details, gotchas, and CI/release notes.
---
iOS
Deployment target: 14.0
Mapbox requires iOS 14.0. Flutter's default is lower, which is why a fresh flutter create + flutter pub add mapbox_maps_flutter build fails.
Set the minimum deployment on the Runner target in Xcode (General → Minimum Deployments → iOS = 14.0). If the project has an ios/Podfile, update the platform line to match:
# ios/Podfile
platform :ios, '14.0'After editing the Podfile, run:
cd ios && pod install && cd ..If you get a Podfile.lock conflict, delete ios/Pods/, ios/Podfile.lock, and re-run pod install.
CocoaPods vs Swift Package Manager
The plugin ships both: ios/mapbox_maps_flutter.podspec (CocoaPods) and ios/mapbox_maps_flutter/Package.swift (SPM). Flutter decides which to use based on your app configuration — new projects on Flutter 3.29+ default to SPM, older projects stay on CocoaPods. Either works; you don't need to choose or vendor dependencies yourself.
To force a mode machine-wide:
flutter config --enable-swift-package-manager # SPM
flutter config --no-enable-swift-package-manager # CocoaPods onlyLocation permission
ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Show your location on the map.</string>If you ever call any "Always" authorization API, you also need NSLocationAlwaysAndWhenInUseUsageDescription. Without the correct key, iOS will reject the permission prompt and CLLocationManager will stay in .notDetermined.
Background modes
The stock plugin does not require any background mode entitlement. Only enable Location updates under Signing & Capabilities → Background Modes if your app genuinely needs background location — each capability triggers additional App Store review scrutiny.
Apple Silicon simulator
No extra setup needed on Flutter 3.16+. If you see building for iOS Simulator, but linking in object file built for iOS, clean and re-run: flutter clean && cd ios && pod deintegrate && pod install.
---
Android
minSdk and compileSdk
// android/app/build.gradle.kts
android {
defaultConfig {
minSdk = 21
targetSdk = 34
}
compileSdk = 34
}The Mapbox Maven repository is configured automatically by the plugin; you do not need to add it yourself.
Permissions
android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />Android 10+ treats location as a runtime permission — use permission_handler or your own flow to request it before enabling the puck.
R8 / ProGuard
The SDK ships consumer ProGuard rules. No app-side configuration is required unless you've disabled consumer rules with android.proguardFiles.disableConsumer.
Platform view modes
Android has multiple platform-view hosting modes (TLHC_VD, TLHC_HC, HC, VD). The default is fine for most apps. If you see black frames during scroll, try switching MapWidget's androidHostingMode — this is an advanced knob, see the plugin's MapWidget docs.
---
Access tokens
Where it goes
const accessToken = String.fromEnvironment('ACCESS_TOKEN');
void main() {
MapboxOptions.setAccessToken(accessToken);
runApp(const MyApp());
}Must run before any MapWidget is constructed — a token set later won't retroactively authenticate an already-created map.
Passing the token at build time
flutter run --dart-define=ACCESS_TOKEN=pk.your_public_token
flutter build ios --dart-define=ACCESS_TOKEN=$MAPBOX_TOKEN
flutter build apk --dart-define=ACCESS_TOKEN=$MAPBOX_TOKENFor a team of developers, a --dart-define-from-file=env.json file (gitignored) is convenient:
// env.json
{ "ACCESS_TOKEN": "pk.your_public_token" }flutter run --dart-define-from-file=env.jsonAdd env.json to .gitignore.
VS Code / Android Studio launch configs
.vscode/launch.json:
{
"configurations": [
{
"name": "Flutter (dev)",
"request": "launch",
"type": "dart",
"args": ["--dart-define-from-file=env.json"]
}
]
}Public vs secret tokens
Use a public token (pk.…) in the app — it's shipped in the binary and can be extracted. Rotate and scope it with URL restrictions in the Mapbox dashboard.
Secret tokens (sk.…) must never ship to the client; they are for server-side tile preprocessing, offline downloads in CI, etc.
Download token for installs
Some release workflows require a secret download token with the DOWNLOADS:READ scope. This is separate from the runtime public token and is not needed at app runtime — only by CI to fetch the native SDK binaries. If the Android build fails with "401 Unauthorized" fetching Mapbox artifacts, configure the download token in ~/.gradle/gradle.properties:
MAPBOX_DOWNLOADS_TOKEN=sk.your_downloads_token---
CI notes
- Cache the
~/.pub-cache,ios/Pods, and Gradle caches across runs — Flutter + Mapbox builds are slow from cold. - For iOS device builds, provision profiles must include the bundle ID; nothing Mapbox-specific.
- Pass tokens via CI secrets, not committed files. Use
--dart-define=ACCESS_TOKEN=${{ secrets.MAPBOX_PUBLIC_TOKEN }}.
---
Release checklist
- ✅ iOS Runner Minimum Deployment = 14.0
- ✅ Android minSdk = 21
- ✅ Public token passed via
--dart-define, not hard-coded - ✅ Location permission purpose strings in
Info.plist - ✅ Location permissions declared in
AndroidManifest.xml - ✅
MapboxOptions.setAccessToken(...)called inmain()beforerunApp