Skip to content

Getting Started

Add maplibre_gl to a Flutter app and get a map on screen. Android and iOS need one platform file each; web needs nothing to start. Later sections cover the web setups that need more: a Content-Security-Policy, a self-hosted copy of the library, or your own JS interop.

Add the dependency

Add maplibre_gl to your project by running this command:

flutter pub add maplibre_gl

Alternatively, add it directly as a dependency in your pubspec.yaml file:

pubspec.yaml
dependencies:
  maplibre_gl: ^0.28.0

Then run flutter pub get to install the package.

Using the development version

To get the latest features and fixes before they are published, depend on the package directly from GitHub.

Warning

The development version is not considered stable and shouldn't be used in production.

Use it as a normal dependency, or temporarily override it under dependency_overrides::

pubspec.yaml
dependencies:
  maplibre_gl:
    git:
      url: https://github.com/maplibre/flutter-maplibre-gl
      ref: main # or a specific commit hash

Android

If you want to show the user's location on the map, add location permissions to the application manifest:

android/app/src/main/AndroidManifest.xml
<manifest>
  <!-- Always include this permission -->
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <!-- Include only if your app benefits from precise location access -->
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
</manifest>

For F-Droid and other builds without Google Play Services, set maplibre_gl.playServicesLocation=false in android/gradle.properties, as shown in User Location.

The map renders with OpenGL ES. To use Vulkan, set maplibre_gl.renderer in the same file, as shown in Rendering backend.

iOS

Add a location usage description to your Info.plist:

ios/Runner/Info.plist
<key>NSLocationWhenInUseUsageDescription</key>
<string>Show your location on the map</string>

Swift Package Manager

The plugin is a Swift package, and Flutter builds it through its Swift Package Manager integration, on by default since Flutter 3.44. A new app needs nothing more. It takes Xcode 15 or later.

If your app turned Swift Package Manager off (flutter config --list shows enable-swift-package-manager: false), turn it back on for all your projects:

flutter config --enable-swift-package-manager

or for this app only, in pubspec.yaml, which also covers CI and the other developers:

flutter:
  config:
    enable-swift-package-manager: true

The next flutter run or flutter build ios adds the Swift packages to ios/Runner.xcodeproj: commit that change. Plugins without a Swift package keep coming from CocoaPods, so the Podfile stays while the app has any. Once every plugin is a Swift package, Flutter suggests removing CocoaPods: run pod deintegrate in ios/ and delete the Podfile.

Add-to-app

A Flutter module inside a native iOS app still uses CocoaPods, since Flutter does not build modules with Swift Package Manager yet (flutter/flutter#146957). The plugin keeps its podspec for these apps, on MapLibre iOS 7.0.0: later versions are not published to CocoaPods.

Web

On web, the plugin renders with MapLibre GL JS, and it loads that library itself: nothing needs to be added to web/index.html. The plugin imports the exact build it is tested against, stylesheet included, before the first map is built.

Upgrading from an older version

If your index.html still has the maplibre-gl.js script and maplibre-gl.css link tags from an earlier setup, remove them. An existing maplibregl global is reused as it is, so a manually pinned copy silently overrides the version the plugin is tested against.

Basic Usage

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: MapLibreMap(
        initialCameraPosition: const CameraPosition(
          target: LatLng(51.5, -0.09),
          zoom: 11,
        ),
        styleString: MapLibreStyles.demo,
      ),
    );
  }
}

Style URL

Pass any MapLibre-compatible style URL to styleString: a hosted provider such as MapTiler or Protomaps, your own tile server, or a style JSON shipped as an asset. See Map Styles.

Web: advanced setup

Requirements

The browser has to provide WebGL2. MapLibre GL JS 6 draws with it and no longer falls back to WebGL1, so a browser without it shows no map at all. Two cases end differently:

  • The browser has no WebGL2 at all, as on Safari and iOS before 15, and on Chrome and Firefox from before 2017. Flutter's own renderer asks for WebGL1 on such a browser, so the app around the map keeps working and only the map goes missing, which is what makes this easy to miss. The plugin logs the cause when this happens.
  • WebGL2 exists but no context can be created, because the browser blocklists it for the GPU driver, or hardware acceleration is switched off. Flutter's renderer asks for WebGL2 there, and does not fall back either, so the whole app stays blank and the library version makes no difference.

Only the first case has a way out: MapLibre GL JS 5, the last major version with the WebGL1 fallback. Point the plugin at a version 5 copy with MapLibreMap.webLibrarySource, exactly as for self-hosting. The plugin keeps working against it, but you then stay off the version it is tested against.

Content-Security-Policy

Skip this if your app has no CSP. If it does, three things need allowing: the plugin imports the library, which a CSP governs like any other script; the style and the tiles are fetched; and MapLibre GL JS runs its tile work in a Web Worker. That worker is built from a blob: URL whenever the library is loaded cross-origin, which is what a CDN is.

script-src 'self' https://unpkg.com ;
connect-src 'self' https://unpkg.com https://your.tile.host ;
worker-src 'self' blob: ;
img-src data: blob: 'self' ;

Self-hosting the library makes the worker same-origin, so blob: is not needed in worker-src and the CDN host drops out of script-src and connect-src.

Self-hosting MapLibre GL JS

If a Content-Security-Policy rules out the CDN, or you prefer serving the library yourself (for example as web assets), point the plugin at your copy before the first map is built:

void main() {
  MapLibreMap.webLibrarySource = const MapLibreJsSource.urls(
    scriptUrl: 'https://your.host/maplibre-gl.mjs',
    styleUrl: 'https://your.host/maplibre-gl.css',
  );
  runApp(const MyApp());
}

MapLibre GL JS 6 is an ES module, so scriptUrl points at the .mjs build. Serve the whole dist directory, not just that one file: the library resolves its worker relative to its own URL, so the worker build has to sit next to it. Check too that your server answers .mjs with a JavaScript MIME type, text/javascript: a module script is refused outright when the type is something else, such as the application/octet-stream some servers still default to.

If the page loads MapLibre GL JS itself, set MapLibreMap.webLibrarySource = const MapLibreJsSource.preloaded(): the plugin then imports nothing and waits for the maplibregl global. An ES module defines no global on its own, so the page has to publish one:

<script type="module">
  globalThis.maplibregl = await import('/your/path/maplibre-gl.mjs');
</script>

Calling MapLibre GL JS yourself

Because the library is loaded by the plugin, the maplibregl global no longer exists at page parse time. An app that calls into MapLibre GL JS with its own JS interop, for example to register a custom protocol with addProtocol, must await MapLibreMap.ensureWebLibraryLoaded() first:

Future<void> main() async {
  if (kIsWeb) {
    await MapLibreMap.ensureWebLibraryLoaded();
    // maplibregl is now usable from JS interop.
  }
  runApp(const MyApp());
}

On Android and iOS MapLibreMap.ensureWebLibraryLoaded() completes immediately, so it is safe to await unconditionally.

PMTiles on web

To read PMTiles sources on web, load the pmtiles script in index.html and register the protocol from Dart. The registration used to be an inline script in index.html, but it needs the maplibregl global, which no longer exists at page parse time, so it moved into main() behind MapLibreMap.ensureWebLibraryLoaded():

web/index.html
<head>
    <!-- ...existing head tags... -->
    <script src="https://unpkg.com/pmtiles@4.4.0/dist/pmtiles.js"></script>
</head>
lib/main.dart
Future<void> main() async {
  if (kIsWeb) {
    await MapLibreMap.ensureWebLibraryLoaded();
    registerPmTilesProtocol('https://your.host/archive.pmtiles');
  }
  runApp(const MyApp());
}

registerPmTilesProtocol is a small piece of JS interop around maplibregl.addProtocol and the pmtiles global. See pmtiles_protocol_web.dart in the example app for a complete implementation, including the conditional import that keeps the app compiling for Android and iOS.

Minimum versions

Platform Minimum version
Flutter 3.44.0
Dart 3.12.0
Android API 24 (Android 7.0)
iOS iOS 15.5
Web WebGL2, see Requirements
JDK 21

The JDK version is for building: the Android side targets Java 21, so JDK 17 fails with invalid source release: 21.