Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

Flutter — Offline Maps

The offline package lets you download a bounded, zoom-limited slice of a style (tiles + glyphs/sprites) to the device so the map keeps working without a network connection. It is exposed through three types: OfflineManager (the entry point), OfflineRegion (a downloaded region's metadata), and DownloadProgress (streamed download state).

OfflineManager cannot be used on web.

This is a thin wrapper over MapLibre Native's offline storage — the API surface below is everything it exposes; there is no additional region-management UI or background-download scheduling built in.

Creating the manager

dart
abstract interface class OfflineManager {
  static Future<OfflineManager> createInstance();

  void dispose();
}
dart
final manager = await OfflineManager.createInstance();
// ...
manager.dispose(); // free native resources when no longer needed, e.g. in State.dispose()

Downloading a region

dart
Stream<DownloadProgress> downloadRegion({
  required String mapStyleUrl,
  required LngLatBounds bounds,
  required double minZoom,
  required double maxZoom,
  required double pixelDensity,
  Map<String, Object?> metadata = const {},
});

downloadRegion returns a Stream<DownloadProgress> — listen to it to drive a progress indicator and detect completion:

dart
final bregenz = const LngLatBounds(
  longitudeWest: 9.589786,
  longitudeEast: 9.766498,
  latitudeSouth: 47.446159,
  latitudeNorth: 47.574776,
);

final stream = manager.downloadRegion(
  mapStyleUrl:
      'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
  bounds: bregenz,
  minZoom: 10,
  maxZoom: 14,
  pixelDensity: 1,
);

await for (final update in stream) {
  if (update.downloadCompleted) {
    print('done: ${update.region}');
    break;
  }
  print('${update.loadedTiles}/${update.totalTiles} '
      '(${((update.progress ?? 0) * 100).toStringAsFixed(0)}%)');
}

DownloadProgress

dart
class DownloadProgress {
  final int loadedBytes;
  final int loadedTiles;
  final int totalTiles;
  final bool totalTilesEstimated;
  final OfflineRegion region;
  final bool downloadCompleted;

  double? get progress; // loadedTiles / totalTiles, or null if totalTilesEstimated
}

totalTiles is a lower bound while totalTilesEstimated is true (early in the download, before the style and tile sources are fully resolved); once the precise count is known, totalTilesEstimated flips to false and progress starts returning a real fraction instead of null.

OfflineRegion

dart
class OfflineRegion {
  final int id;
  final LngLatBounds bounds;
  final double minZoom;
  final double maxZoom;
  final double pixelRatio;
  final String styleUrl;
}

This is the metadata record for a downloaded region — returned inside DownloadProgress.region, and by the lookup/listing calls below.

Managing downloaded regions

dart
Future<OfflineRegion> getOfflineRegion({required int regionId});
Future<List<OfflineRegion>> listOfflineRegions();
Future<void> resetDatabase(); // deletes the existing database and re-initializes it
dart
final region = await manager.getOfflineRegion(regionId: 1);
final all = await manager.listOfflineRegions();

Android-only region-database maintenance calls (no-ops to avoid on iOS — check Platform.isIOS before calling):

dart
Future<List<OfflineRegion>> mergeOfflineRegions({required String path});
Future<void> packDatabase();
void runPackDatabaseAutomatically({required bool enabled}); // enabled by default

Ambient cache

Separate from named offline regions, MapLibre also maintains an ambient tile cache (tiles seen during normal map browsing, evictable, not guaranteed to persist):

dart
void setOfflineTileCountLimit({required int amount}); // default limit: 6,000 tiles
Future<void> setMaximumAmbientCacheSize({required int bytes});
Future<void> clearAmbientCache();
Future<void> invalidateAmbientCache(); // forces re-validation, doesn't clear

Using a downloaded region

There's no dedicated "load this region offline" call — once tiles for a LngLatBounds are downloaded, point MapOptions.initStyle at the same style URL and constrain the camera to the same bounds with MapOptions.maxBounds; the map will read from the local offline store for any tile already downloaded, and hit the network for anything outside that:

dart
MapMetricsView(
  options: MapOptions(
    initStyle: region.styleUrl,
    maxBounds: region.bounds,
    initCenter: Position(9.717795, 47.5041), // Bregenz: lng, lat
    initZoom: 12,
    maxZoom: region.maxZoom,
  ),
)

See also