flutter_map_vector_tiles 2.2.0 copy "flutter_map_vector_tiles: ^2.2.0" to clipboard
flutter_map_vector_tiles: ^2.2.0 copied to clipboard

Vector tiles for flutter_map: a clean, self-contained MapLibre-style vector tile layer with isolate-based decoding, raster tile caching and screen-space label collision.

πŸ—ΊοΈ flutter_map_vector_tiles #

pub package license: BSD-3-Clause flutter_map

Vector tiles for flutter_map. A clean, self-contained rewrite of the ideas behind vector_map_tiles β€” built for flutter_map β‰₯ 8 and modern Flutter (Impeller).

Render MapLibre / Mapbox GL styles (MapTiler, OpenFreeMap, OpenMapTiles, Stadia, Protomaps, …) straight from MVT vector tile sources β€” as a plain flutter_map layer. flutter_map keeps owning the camera, gestures and all your other layers; this package only draws the map.


✨ Why this package? #

πŸ“¦ One package MVT decoding, style engine and renderer in a single dependency β€” no renderer/cache/executor satellites
πŸš€ Smooth interaction Geometry is rasterized once per tile into GPU-resident images (Picture.toImageSync); pan, zoom and rotate are just textured quads
πŸ” Crisp labels Text & icons are drawn per-frame in screen space: upright under rotation, sharp at fractional zoom, with one global collision pass β€” no duplicated or clipped labels at tile seams
🌫️ No white flashes New tiles fade in while ancestor imagery is kept underneath; fast zoom-ins render instantly from already-decoded parent tiles
🎚️ Correct MapLibre zoom semantics The default TileOffset.maplibre renders 512px-convention styles exactly as their authors designed them
🧡 Isolate pipeline Tiles are decoded & trimmed on a worker-isolate pool (a yielding event-loop queue on web), viewport-centre first; cancellation is a state, never an exception in your crash reporting
πŸ’Ύ Deterministic caching LRU memory caches with byte budgets + a size-capped disk cache with no index files to corrupt; every ui.Image is disposed on eviction
✈️ Works offline The style bundle and recently viewed tiles are cached on disk (native platforms): places you visited keep rendering with no network at all
🌐 All six platforms Android, iOS, macOS, Linux, Windows and web β€” see Web support for what differs in the browser
πŸ›‘οΈ Tolerant style reader Unknown layer types and exotic expressions degrade per-layer with a warning β€” one weird layer never kills your whole map

πŸš€ Quick start #

1. Install #

dependencies:
  flutter_map: ^8.2.0
  flutter_map_vector_tiles: ^2.2.0

2. Load a style & drop in the layer #

import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_vector_tiles/flutter_map_vector_tiles.dart' as vt;
import 'package:latlong2/latlong.dart';

// Load the style once β€” MapTiler shown, any MapLibre style URL works.
final style = await vt.StyleReader(
  uri: 'https://api.maptiler.com/maps/streets-v2/style.json?key={key}',
  apiKey: myMapTilerKey,
).read();

// Use it like any other flutter_map layer:
FlutterMap(
  options: MapOptions(
    initialCenter: style.center ?? const LatLng(48.137, 11.575),
    initialZoom: style.zoom ?? 12,
    maxZoom: 21,
  ),
  children: [
    vt.VectorTileLayer(
      theme: style.theme,
      tileProviders: style.providers,
      rasterSources: style.rasterSources,
      sprites: style.sprites,
    ),
    // Show what the style's sources ask for β€” most providers require it.
    SimpleAttributionWidget(
      source: Text(style.attributions.map((a) => a.text).join(' Β· ')),
    ),
    // ...your markers, polylines, etc.
  ],
);

3. Clean up #

@override
void dispose() {
  style.dispose(); // releases HTTP clients & sprite images
  super.dispose();
}

▢️ A runnable app lives in example/:

cd example
flutter run --dart-define=MAPTILER_KEY=yourKey

🌍 Tested style providers #

Provider Style URL shape Notes
🟒 MapTiler https://api.maptiler.com/maps/<mapId>/style.json?key={key} works with custom map styles
🟒 OpenFreeMap https://tiles.openfreemap.org/styles/liberty free, no key needed
🟒 Stadia Maps https://tiles.stadiamaps.com/styles/osm_bright.json?api_key={key}
🟒 ArcGIS / Esri https://…/VectorTileServer/resources/styles/root.json relative ../../ sources, tile/{z}/{y}/{x} templates and sprites resolve correctly β€” verified against World_Basemap_v2
🟒 Self-hosted (TileServer GL, Martin, …) any MapLibre style.json verified against the MapLibre demo tiles; relative tile templates supported
🟒 Protomaps hosted API https://api.protomaps.com/styles/v5/light/en.json?key={key} verified against the v5 light style; the style embeds the key in an absolute …/tiles/v4/{z}/{x}/{y}.mvt?key=… template. On web, allow-list your origin per key in the Protomaps account portal β€” localhost is exempt
🟒 PMTiles archives pmtiles://https://…/planet.pmtiles source URLs in any style single-file archives served via HTTP range requests β€” verified against the Protomaps sample archives; gzip-internal archives only (brotli/zstd are rejected)

The reader is tolerant either way: unsupported layer types, paint properties and expressions are skipped per-layer with a warning β€” one weird layer never kills the whole style.

βš™οΈ Configuration #

Everything has sensible defaults β€” override what you need:

vt.VectorTileLayer(
  theme: style.theme,
  tileProviders: style.providers,
  rasterSources: style.rasterSources,          // satellite/hybrid imagery
  sprites: style.sprites,
  tileOffset: vt.TileOffset.maplibre,          // 512px style convention (default)
  concurrency: 3,                              // decoding isolates
  diskCacheMaximumSizeInBytes: 50 * 1024 * 1024,
  diskCacheTtl: const Duration(days: 14),
  memoryCacheMaxBytes: 24 * 1024 * 1024,
  tileFadeDuration: const Duration(milliseconds: 150),
  showLabels: true,
  logger: const vt.Logger.console(),           // see style warnings in debug
)
Parameter Default What it does
tileOffset TileOffset.maplibre zoom relation between map and style β€” see below πŸ‘‡
concurrency 3 worker isolates decoding tiles off the UI thread (ignored on web)
diskCacheMaximumSizeInBytes 50 MB 0 disables disk caching (no effect on web)
diskCacheTtl 14 days freshness window: younger tiles skip the network; older ones are refetched but kept as offline fallback β€” ⚠️ respect your tile provider's terms
cachePath app support dir supply your own directory path to control/clear it (ignored on web)
memoryCacheMaxBytes 24 MB decoded tile budget per source (the caches are shared process-wide; the most recently mounted layer's value wins)
tileFadeDuration 150 ms Duration.zero disables fade-in
showLabels true disables the whole symbol pass when false; toggling it re-lays-out the tiles already on screen

StyleReader options worth knowing:

  • apiKey β€” substituted for {key} in the style URI and every URL the style references. For mapbox:// URIs (style ids, sprite bases, tileset sources β€” expanded to api.mapbox.com automatically) it becomes the access token.
  • headers β€” extra HTTP headers sent with the style, TileJSON and sprite requests and forwarded to the created tile providers, for header-authenticated services (e.g. Authorization).

🎚️ Understanding TileOffset #

MapLibre renders 512px tiles, so at the same visual scale a MapLibre zoom is one lower than flutter_map's. Styles from MapTiler & friends are authored against that convention.

  • TileOffset.maplibre (default) β€” text sizes, road widths and layer zoom ranges match the style author's intent exactly.
  • TileOffset.none β€” evaluates the style at flutter_map's zoom directly; everything appears one zoom earlier/larger (the legacy vector_map_tiles default, if you need visual parity with it).

✈️ Offline behaviour #

Everything you looked at recently keeps working without network:

  • Style bundle β€” StyleReader caches style.json, TileJSON and sprites on disk (stale-while-revalidate): the cached copy is served instantly β€” including fully offline β€” and refreshed in the background once older than refreshAfter (12 h default). Opt out with StyleReader(cache: false).
  • Tiles β€” served from the disk cache while fresh; when a network fetch fails, an expired cached tile is served instead of a blank one. Stale tiles are only ever deleted by the size cap (oldest first), never by age alone.
  • Durable location β€” both caches default to the application support directory, which the OS doesn't purge (unlike the temp directory).

This is a visited-places cache, not region pre-download. For guaranteed offline regions, bundle tiles and serve them through the VectorTileProvider interface (e.g. MBTiles/PMTiles) alongside an asset:// style.

Disk caching β€” and with it the offline behaviour above β€” is native-only; see Web support for what applies in the browser.

🌐 Web support #

The layer runs on Flutter web with the CanvasKit/Skwasm renderer β€” the default since Flutter 3.29. (Do not force the removed HTML renderer on Flutter 3.27/3.28: it lacks Picture.toImageSync.) What differs from native:

  • No persistent cache β€” cachePath, diskCacheTtl, diskCacheMaximumSizeInBytes and StyleReader(cache: …) are no-ops. Tiles and the style bundle rely on the in-memory caches plus the browser's own HTTP cache instead.
  • Decoding runs on the event loop β€” a yielding queue replaces the worker-isolate pool (concurrency is ignored).
  • CORS β€” the browser fetches style.json, TileJSON, sprites and tiles directly, so every host involved must send Access-Control-Allow-Origin. MapTiler, OpenFreeMap and Stadia do; self-hosted tile servers need it configured. PMTiles archive hosts additionally need range requests to pass CORS (Range in Access-Control-Allow-Headers when preflighted).
  • PMTiles gunzip uses the browser's native DecompressionStream (available in every browser that runs Flutter web).

πŸ”Œ Custom tile sources #

No style URL? Any {z}/{x}/{y} MVT endpoint works β€” build the theme yourself and wire providers manually:

vt.VectorTileLayer(
  theme: vt.ThemeReader(logger: const vt.Logger.console()).read(myStyleJson),
  tileProviders: vt.TileProviders({
    'openmaptiles': vt.NetworkVectorTileProvider(
      urlTemplate: 'https://tiles.example.com/{z}/{x}/{y}.pbf?key=$key',
      maximumZoom: 14, // the source's max β€” higher zooms overzoom this data
    ),
  }),
)

PMTiles single-file archives work out of the box: styles with pmtiles://https://…/planet.pmtiles source URLs just load, or open an archive directly:

final provider = await vt.PmTilesVectorTileProvider.open(
  'https://tiles.example.com/planet.pmtiles',
);
// β†’ vt.TileProviders({'mySource': provider})

There's also MemoryVectorTileProvider (tests, bundled offline regions) and a small VectorTileProvider interface for anything else (MBTiles, …).

🎨 Style support #

Layer types: background, fill (incl. fill-pattern), line (incl. line-pattern, dashes, casing), symbol (incl. curved line text, text-variable-anchor / text-radial-offset), circle, and raster β€” raster sources inside vector styles (satellite/hybrid imagery) draw at their layer position with raster-opacity, brightness/contrast/saturation/hue-rotate matching MapLibre's shader math (fill-extrusion renders as flat fill; hillshade, heatmap and sky are skipped with a log line).

Icons: SDF sprite sheets ("sdf": true) are thresholded and tinted per icon-color, icon-halo-color and icon-halo-width β€” dark MapLibre styles ship their icons this way. Ordinary sprites are drawn with the colours baked into the sheet.

Road labels curve glyph-by-glyph along their line with MapLibre semantics: text-max-angle rejects labels on sharp bends, text-keep-upright flips reading direction, and text-rotation-alignment: viewport keeps shield text horizontal. Nearly straight windows are drawn as a single rotated string for speed; scripts with contextual shaping (Arabic, Indic, …) fall back to straight placement so glyphs are never mis-joined.

Expressions: the practical MapLibre set β€” get/has, comparisons, all/any/case/match/coalesce, step/interpolate (linear, exponential, cubic-bezier), math, string & color operators, let/var, legacy filters, legacy {stops} functions and {token} templates.

πŸ—οΈ Architecture #

style.json ─► StyleReader ─► compiled Theme (expressions β†’ closures)
camera ─► visible display tiles ─► data tiles (shared, LRU-cached)
   bytes ◄─ disk cache ◄─ network        ─► isolate: decode + trim
   PreparedTile ─► rasterize once ─► GPU image ─► textured quad per frame
   symbols ─────► per-frame screen-space label pass (global collision)

On web the disk cache tier is absent and decoding runs on a yielding event-loop queue instead of isolates; everything else is identical.

The full rendering model and the reasoning behind each departure from vector_map_tiles is documented in doc/ARCHITECTURE.md. πŸ“–

πŸ†š vector_map_tiles #

vector_map_tiles (stable) this package
Packages 3 (vector_map_tiles, vector_tile_renderer, executor_lib) + stash caching 1
Labels baked into tile rasters / per-tile collision screen-space pass, global collision, upright text
Zoom flicker white flash on fast zoom (#147) ancestor retention + provisional rendering
Cancellation CancellationException reaches crash reporting (#205) a state, never an exception
Style zoom evaluated at flutter_map zoom (1 off vs. MapLibre) TileOffset.maplibre default
Rasters async image encode Picture.toImageSync (stays on GPU)

πŸ› Troubleshooting #

  • Blank map, no errors β†’ pass logger: const vt.Logger.console() to both StyleReader and VectorTileLayer; most often the style's source ids don't match your TileProviders keys, or your API key is invalid (HTTP 403s are logged, keys redacted).
  • Labels/roads look bigger than in MapLibre β†’ you're probably using TileOffset.none with a 512px-convention style; use the default.
  • Stale data after changing styles β†’ the disk cache keys by URL; a changed {key} or map id is a different URL, so usually nothing to do. Supply cachePath if you want to wipe it yourself.
  • Blank map on web β†’ open the browser console; missing Access-Control-Allow-Origin headers on the style or tile host block every request (see Web support).

🀝 Contributing #

Issues and PRs are welcome! Please run dart analyze && flutter test before submitting β€” the suite covers the MVT decoder, expression engine, caches, grid math and tile store. Run flutter test --platform chrome too when touching anything platform-sensitive (requires Chrome).

πŸ“„ License #

BSD 3-Clause Β© 2026 Jonas Grunau

2
likes
0
points
865
downloads

Publisher

unverified uploader

Weekly Downloads

Vector tiles for flutter_map: a clean, self-contained MapLibre-style vector tile layer with isolate-based decoding, raster tile caching and screen-space label collision.

Repository (GitHub)
View/report issues

Topics

#flutter-map #vector-tiles #maplibre #map #pmtiles

License

unknown (license)

Dependencies

characters, flutter, flutter_map, http, latlong2, meta, path_provider

More

Packages that depend on flutter_map_vector_tiles