flutter_map_vector_tiles 2.0.1
flutter_map_vector_tiles: ^2.0.1 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 #
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.0.1
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,
),
// ...your markers, polylines, attribution, 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 | https://api.protomaps.com/styles/β¦?key={key} |
the hosted API uses the same URL shapes and should work (untested); pmtiles:// archives are not supported |
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 |
tileFadeDuration |
150 ms | Duration.zero disables fade-in |
showLabels |
true |
disables the whole symbol pass when false |
ποΈ 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 legacyvector_map_tilesdefault, if you need visual parity with it).
βοΈ Offline behaviour #
Everything you looked at recently keeps working without network:
- Style bundle β
StyleReadercaches 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 thanrefreshAfter(12 h default). Opt out withStyleReader(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,diskCacheMaximumSizeInBytesandStyleReader(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 (
concurrencyis 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.
π 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
),
}),
)
There's also MemoryVectorTileProvider (tests, bundled offline regions)
and a small VectorTileProvider interface for anything else (MBTiles,
PMTiles, β¦).
π¨ 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. π
π vs. 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 bothStyleReaderandVectorTileLayer; most often the style's source ids don't match yourTileProviderskeys, or your API key is invalid (HTTP 403s are logged, keys redacted). - Labels/roads look bigger than in MapLibre β you're probably using
TileOffset.nonewith 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. SupplycachePathif you want to wipe it yourself. - Blank map on web β open the browser console; missing
Access-Control-Allow-Originheaders 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