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.27.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>

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>

The plugin ships both a Swift package and a CocoaPods podspec, so it works with Flutter's Swift Package Manager integration and with CocoaPods. Nothing to configure either way: apps already on CocoaPods need no migration, and apps with SPM enabled need no Podfile on account of this plugin.

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.29.0
Dart 3.7.0
Android API 21 (Android 5.0)
iOS iOS 13
Web WebGL2, see Requirements