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
abstract interface class OfflineManager {
static Future<OfflineManager> createInstance();
void dispose();
}final manager = await OfflineManager.createInstance();
// ...
manager.dispose(); // free native resources when no longer needed, e.g. in State.dispose()Downloading a region
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:
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
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
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
Future<OfflineRegion> getOfflineRegion({required int regionId});
Future<List<OfflineRegion>> listOfflineRegions();
Future<void> resetDatabase(); // deletes the existing database and re-initializes itfinal 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):
Future<List<OfflineRegion>> mergeOfflineRegions({required String path});
Future<void> packDatabase();
void runPackDatabaseAutomatically({required bool enabled}); // enabled by defaultAmbient cache
Separate from named offline regions, MapLibre also maintains an ambient tile cache (tiles seen during normal map browsing, evictable, not guaranteed to persist):
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 clearUsing 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:
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
- MapOptions reference —
maxBoundsandinitStyle - MapController reference —
setStyleUrito switch to an offline-downloaded style at runtime