A high-performance Flutter map library with native raster and vector tile rendering, featuring OpenFreeMap integration, persistent background isolates, and advanced performance optimizations.
- πΊοΈ Dual Rendering Modes: Native raster (OSM) and vector (Mapbox Vector Tiles) rendering
- π Markers: Any Flutter widget (or plain text) anchored to lat/lon via
MarkerManagerβ with tap/long-press gestures (hand cursor on hover) and marker-following overlays - π High Performance: Persistent HTTP isolate with TCP connection reuse
- π― Smart Caching: Memory + disk (Hive) with intelligent eviction
- π OpenFreeMap Integration: Free, no API key required vector tiles
- π± Cross-Platform: iOS, Android, Web, macOS, Linux, Windows
- π·οΈ Labels & Icons: Point labels, line labels (road names), and sprite icons
- π Zoom Animations: Google Maps-style two-phase transitions β blurred hold while tiles load, then scale reveal. Two intensity levels (
scale/crossfade) - π§΅ Background Processing: Compute-based protobuf parsing on separate threads
- π Pre-loading: Intelligent adjacent zoom level pre-loading
dependencies:
fosm:
git:
url: https://github.com/yourusername/fosm.gitimport 'package:fosm/fosm.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initMap(); // Initialize Hive cache
runApp(const MyApp());
}MapView(
latLng: LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
minZoom: 1,
maxZoom: 19,
showZoomControls: true,
animateZoom: true,
)MapView(
latLng: LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
minZoom: 1,
maxZoom: 19,
vectorStyle: openFreeMapLiberty, // Built-in preset
showZoomControls: true,
animateZoom: true,
onZoomChanged: (zoom) {
print('Zoom level: $zoom');
},
)const customStyle = VectorMapStyle(
id: 'my-custom-style',
styleUrl: 'https://tiles.openfreemap.org/styles/bright',
);
MapView(
latLng: LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
vectorStyle: customStyle,
)Own a MarkerManager, pass it to the map, and mutate it at runtime β the
map re-renders on every change:
final markers = MarkerManager();
MapView(
latLng: const LatLng(latitude: 47.4358, longitude: 8.4737),
zoom: 10,
markers: markers,
);
// Any widget, anchored so its bottom-center tip sits on the coordinate:
markers.add(
const Marker(
point: LatLng(latitude: 47.4358, longitude: 8.4737),
alignment: Alignment.bottomCenter,
child: Icon(Icons.location_on, size: 40, color: Colors.red),
),
);
// Or plain text:
markers.add(Marker.text('Zurich', const LatLng(latitude: 47.3769, longitude: 8.5417)));
// Remove individually (by identity), filter, or clear:
markers.remove(firstMarker);
markers.removeWhere((m) => /* β¦ */);
markers.clear();Markers render above the tile grid (below vector labels) and track the
camera on every pan and zoom. Markers whose anchor leaves the viewport
are culled β they are not built, laid out, or painted until visible
again. alignment picks which point of the widget sits on the
coordinate (default: center; use Alignment.bottomCenter for pins).
Marker children are ordinary widgets, so buttons and gesture handlers
work inside them.
Markers accept onTap / onLongPress callbacks, and any marker with an
overlayBuilder gets a tap-to-toggle overlay ("info window") β any
Flutter widget, anchored to the marker and following it across pans and
zooms:
markers.add(
Marker(
point: const LatLng(latitude: 47.4358, longitude: 8.4737),
alignment: Alignment.bottomCenter,
onTap: () => print('tapped'), // fires alongside the toggle
onLongPress: () => print('long press'),
overlayBuilder: (context) => const Card(
child: Padding(
padding: EdgeInsets.all(8),
child: Text('Hello from Zurich'),
),
),
child: const Icon(Icons.location_on, size: 40, color: Colors.red),
),
);Behavior is shaped per-marker with MarkerOverlayConfig:
| Option | Default | Behavior |
|---|---|---|
removeOnMove |
false |
true dismisses the overlay as soon as the camera changes (pan, zoom, or programmatic); false keeps it anchored while it follows the marker |
closeOnMapTap |
true |
dismisses the overlay when the user taps the bare map or a marker without its own overlay; taps inside the overlay never dismiss it |
anchor |
above |
side of the marker the overlay sits on: above, below or center |
offset |
Offset(0, 8) |
extra gap between marker and overlay |
animationDuration |
200 ms |
fade + scale entrance; Duration.zero shows instantly |
One overlay is visible at a time. The manager exposes programmatic
control and listeners (which also fire for automatic dismissals like
removeOnMove and map taps):
markers.showOverlay(myMarker); // returns false if marker has no overlayBuilder
markers.hideOverlay();
markers.overlayMarker; // whose overlay is open, or null
markers.onOverlayShown = (marker) { /* β¦ */ };
markers.onOverlayHidden = (marker) { /* β¦ */ };Removing a marker (or clearing the manager) while its overlay is open hides the overlay automatically.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Interface β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β MapView Widget β β
β β ββββββββββββββ ββββββββββββββ ββββββββββββββββββ β β
β β β Gesture β β Zoom β β Label β β β
β β β Handler β β Controls β β Overlay β β β
β β ββββββββββββββ ββββββββββββββ ββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tile Manager β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
β β Grid Calc β β Memory Cache β β Pre-loading β β
β β (viewport) β β (LRU, 200) β β Manager β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Rendering Pipeline β
β ββββββββββββββββββ ββββββββββββββββββββββββββ β
β β Raster Mode β β Vector Mode β β
β β β β β β
β β Image Decode β β MVT Parse β Style Eval β β
β β (PNG/JPEG) β β β Canvas Render β β
β ββββββββββββββββββ ββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Isolate Architecture β
β ββββββββββββββββββββ ββββββββββββββββββββββββββ β
β β HTTP Isolate β β Compute Isolates β β
β β (Persistent) β β (Per-Call) β β
β β β β β β
β β β’ TCP Reuse β β β’ MVT Parsing β β
β β β’ Connection β β β’ CPU-intensive work β β
β β Pooling β β β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FOSM uses a sophisticated isolate strategy to keep the UI responsive while performing heavy operations.
A long-lived background isolate that handles ALL network I/O:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β TileManager β
β ββ Visible tile request ββββββββββββββ β
β ββ Preload request βββββββββββββββββββ β
β ββ β
βββββββββββββββββββββββββββββββββββββββββΌβΌββββββββββββββββββββββ
ββ
ββββββββββββββββββββββ
β SendPort(url) β
βΌ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HTTP Isolate (Background) β
β β
β Persistent HttpClient β
β ββ Connection timeout: 10s β
β ββ Idle timeout: 30s β
β ββ TCP Connection Pool ββββββββββββββββββββββββββββββ β
β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β’ Reuses TCP connections across hundreds of ββ β
β β tile requests ββ β
β β β’ Avoids TLS handshake overhead ββ β
β β β’ Reduces latency by ~50-100ms per request ββ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β Response: Uint8List βββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β SendPort(bytes)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β Decode & Render β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Benefits:
- β TCP connection reuse (no repeated TLS handshakes)
- β Reduced latency (~50-100ms saved per request)
- β Network I/O completely off main thread
- β Single isolate for all tile types (raster + vector)
Short-lived isolates for CPU-intensive work:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β VectorTileRuntime β
β ββ decodeMvtAsync(bytes) ββββββββββββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌβββββ
β
compute() spawns β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Compute Isolate (Temporary) β
β β
β decodeVectorTile(bytes) β
β ββ ProtobufReader (parse MVT) β
β ββ Extract layers, features, properties β
β ββ Decode geometry (coordinates) β
β ββ Build DecodedVectorTile βββββββββββββββββββββββββ β
β β β
β Duration: ~2-5ms β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββΌββββββββ
β
Return value β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Thread β
β β
β DecodedVectorTile β
β ββ Render on Canvas (fills, lines, labels, icons) β
β Duration: ~15-25ms β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why not persistent parse isolate?
- Compute isolates use
Isolate.exit()which returns values efficiently - Spawn overhead (~1-3ms) is negligible vs parsing time (2-5ms)
- Persistent isolates have complex serialization issues with Dart objects
- Tests work reliably with compute()
On web, isolates work differently:
// HTTP Isolate: Disabled (browsers handle connection pooling)
if (kIsWeb) {
// Use Dio on main thread
bytes = await downloadTileBytes(url);
} else {
// Use persistent isolate on native
bytes = await _httpIsolate.fetchUrl(url);
}
// MVT Parsing: compute() runs on main thread (no isolates on web)
// But we yield between operations to keep UI responsiveMVT Bytes (.pbf)
β
βΌ
βββββββββββββββββββ
β Protobuf Parse β compute() isolate (native)
β β or main thread (web)
βββββββββββββββββββ
β
βΌ
DecodedVectorTile
β
ββ Layers (water, roads, buildings, etc.)
ββ Features with properties
ββ Geometry (coordinates)
β
βΌ
βββββββββββββββββββ
β Style Eval β Apply Mapbox Style Spec
β β - Expressions (interpolate, match, etc.)
β β - Filters
β β - Paint properties
βββββββββββββββββββ
β
βΌ
βββββββββββββββββββ
β Canvas Render β Chunked rendering (8 layers/batch)
β β - Fills (polygons)
β β - Lines (roads, rivers)
β β - Circles (POIs)
β β - Yield every batch (web)
βββββββββββββββββββ
β
βΌ
ui.Picture
β
βΌ
βββββββββββββββββββ
β toImage(256x256)β GPU β CPU readback
βββββββββββββββββββ
β
βΌ
ui.Image (cached)
To prevent UI freezes on web, rendering is split into batches:
Future<ui.Picture> renderAsync({
required DecodedVectorTile decoded,
required int z, int x, int y,
}) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
var layerCount = 0;
for (final layer in visibleLayers) {
_paintLayer(canvas, layer, decoded);
layerCount++;
// Yield every 8 layers on web
if (kIsWeb && layerCount % 8 == 0) {
await Future<void>.delayed(Duration.zero);
}
}
return recorder.endRecording();
}Performance Impact:
BEFORE: [βββββββββββββββββββββββββββββββββββββββ] 25ms freeze
AFTER: [βββ][yield][βββ][yield][βββ][yield][βββ] ~3ms chunks
Labels are rendered as an overlay (not baked into tiles):
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Viewport Canvas β
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β Tile Image β β Tile Image β β Tile Image β β
β β (256x256) β β (256x256) β β (256x256) β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Label Overlay β β
β β β β
β β β’ Point Labels (cities, POIs) β β
β β β’ Line Labels (road names, rivers) β β
β β β’ Icons (airports, stations, etc.) β β
β β β β
β β Collision Detection: β β
β β ββββββββββ β β
β β β Label ββββ Checks overlap with placed labels β β
β β ββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Line Labels:
- Sampled every 250px along paths
- Rotated to follow road/river direction
- Flipped if upside-down (keeps text readable)
Collision Detection:
- Uniform grid (72px cells)
- First-come-first-served placement
- Prevents label overlap
final LinkedHashMap<String, ui.Image> _memoryCache = LinkedHashMap();
static const int maxMemoryCachedTiles = 200;
void _trimMemoryCache() {
while (_memoryCache.length > maxMemoryCachedTiles) {
final oldestKey = _memoryCache.keys.first;
final image = _memoryCache.remove(oldestKey);
if (!isVisible(oldestKey)) {
image.dispose(); // Free GPU memory
}
}
}Benefits:
- O(1) access time
- Automatic eviction of least-recently-used tiles
- GPU memory management (dispose off-screen tiles)
final Map<String, Uint8List> _byteCache = {};
static const int maxByteCacheBytes = 50 * 1024 * 1024; // 50MB
void _storeInByteCache(String key, Uint8List bytes) {
_byteCache[key] = bytes;
_trimByteCache();
}Purpose:
- Store pre-loaded adjacent zoom tiles (Β±1 levels)
- Instant decode when zooming (no network wait)
- Compressed bytes (PNG/MVT) use less memory than decoded images
await storeTile(key, tile, bytes); // Write
final bytes = await storedTileBytes(key); // ReadFeatures:
- Persistent across app restarts
- Separate namespace per vector style
- Automatic corruption detection (delete + re-download)
// Sort tiles by distance from viewport center
final tiles = visibleTiles
..sort((a, b) => a.distanceToCenter.compareTo(b.distanceToCenter));
for (final tile in tiles) {
_scheduleLoad(tile);
}Result: Center tiles render first, edges fill in progressively.
// Only fetch raster tiles if visible at current zoom
final hasVisibleRaster = layers.any((l) =>
l.type == LayerType.raster &&
l.isVisible &&
zoom >= l.minZoom &&
zoom <= l.maxZoom);
if (hasVisibleRaster) {
// Fetch raster tiles
}Impact: OpenFreeMap Liberty's relief layer only visible at zoom < 5. Skipped at zoom 5+, saving network requests.
// Skip dash computation for hairline widths
if (hasDash && width >= 0.5) {
_drawDashed(canvas, path, paint);
} else {
canvas.drawPath(path, paint);
}Why: Dash patterns imperceptible below 0.5px width.
static const int maxConcurrentDecodes = 3;
Future<ui.Image> decoder(bytes, z, x, y) async {
await _waitForDecodeSlot();
try {
return await _decodeAndRender(bytes, z, x, y);
} finally {
_releaseDecodeSlot();
}
}Purpose: Prevent overwhelming the GPU with simultaneous toImage() calls.
// Pre-load Β±1 zoom levels after 500ms idle
if (idleTime > 500ms) {
_preloadAdjacentZoom();
}
// Max 20 concurrent preloads
static const int maxConcurrentPreloads = 20;Result: Smooth zoom transitions with instant tile availability.
lib/
βββ fosm.dart # Public API exports
βββ src/
βββ api/
β βββ tile.dart # Tile data class
β βββ tile_manager.dart # Grid calculation & loading
β βββ tile_source.dart # TileFetcher typedef
β βββ geo_point.dart # LatLng class
β βββ marker.dart # Marker model
β βββ marker_manager.dart # Marker collection (ChangeNotifier)
β
βββ view/
β βββ map_view.dart # Main map widget
β βββ render.dart # CustomPainters (tiles, labels)
β βββ marker_layer.dart # Widget markers + viewport culling
β βββ zoom_controls.dart # +/- buttons
β
βββ vector/
β βββ mvt/
β β βββ protobuf_reader.dart # Protobuf decoder
β β βββ vector_tile.dart # MVT parser
β β
β βββ render/
β β βββ vector_tile_runtime.dart # Tile lifecycle
β β βββ vector_tile_renderer.dart # Canvas rendering
β β βββ label_overlay.dart # Labels & icons
β β βββ sprite_atlas.dart # Icon sprites
β β
β βββ style/
β βββ style_loader.dart # Load Mapbox style JSON
β βββ expression.dart # Style expressions
β βββ css_color.dart # Color parsing
β
βββ isolate/
β βββ http_isolate.dart # HTTP isolate (stub)
β βββ http_isolate_native.dart # HTTP isolate (native)
β βββ mvt_worker.dart # MVT parsing (compute)
β
βββ common/
βββ utils.dart # Helpers
βββ osm_transformation_utilities.dart # Math
βββ cache_tile_mixin.dart # Hive cache
test/
βββ vector/
β βββ vector_tile_test.dart
β βββ style_parser_test.dart
β βββ expression_test.dart
βββ marker_manager_test.dart
βββ marker_layer_test.dart
βββ tile_manager_test.dart
- β
background- Solid color background - β
fill- Polygon fills - β
line- Lines (roads, rivers, borders) - β
circle- Circles (POIs) - β
symbol- Text labels and icons - β
raster- Raster imagery - β³
fill-extrusion- 3D buildings (planned) - β
heatmap- Not supported - β
hillshade- Not supported
- β
Arithmetic:
+,-,*,/ - β
Comparison:
==,!=,<,<=,>,>= - β
Logical:
all,any,none - β
Interpolation:
interpolate(linear, exponential) - β
Matching:
match,step,case,coalesce - β
Property access:
get,has - β
Type conversion:
to-number,to-string,to-color - β
String:
concat
Fill:
fill-color,fill-opacity,fill-outline-color
Line:
line-color,line-opacity,line-widthline-dasharray,line-cap,line-join
Circle:
circle-color,circle-opacity,circle-radiuscircle-stroke-color,circle-stroke-width
Symbol:
text-color,text-halo-color,text-halo-widthtext-size,text-font,text-letter-spacingicon-image,icon-size
FOSM includes built-in support for OpenFreeMap:
// Built-in preset
MapView(
vectorStyle: openFreeMapLiberty,
// ...
)Features:
- β Free, no API key required
- β Vector tiles (Mapbox Vector Tiles format)
- β Multiple styles: Liberty, Bright, Dark, Positron
- β Global coverage
- β High-performance CDN
Attribution Required:
// Automatically displayed when using vectorStyleMapView(
latLng: LatLng(latitude: 0, longitude: 0),
zoom: 2,
tileFetcher: (z, x, y) async {
final url = 'https://my-tile-server.com/$z/$x/$y.png';
return await downloadTileBytes(url);
},
)const myStyle = VectorMapStyle(
id: 'my-style',
styleUrl: 'https://example.com/style.json',
);
MapView(
vectorStyle: myStyle,
// ...
)MapView(
animateZoom: false,
zoomAnimationDuration: Duration(milliseconds: 200),
// ...
)MapView(
onZoomChanged: (zoom) {
print('Current zoom: $zoom');
setState(() => _currentZoom = zoom);
},
)# Run all tests
flutter test
# Run with coverage
flutter test --coverage
# Run specific test file
flutter test test/vector/vector_tile_test.dartTest Coverage: 86 tests passing
- Vector tile parsing
- Style expression evaluation
- Tile manager logic
- Grid calculations
- Cache behavior
| Operation | Time |
|---|---|
| HTTP fetch (cached connection) | ~50ms |
| MVT protobuf parse | ~2-5ms |
| Style evaluation + render | ~15-25ms |
toImage(256x256) |
~5-15ms |
| Total per tile | ~70-95ms |
| Operation | Time |
|---|---|
| HTTP fetch (browser) | ~100ms |
| MVT protobuf parse | ~2-5ms |
| Style evaluation + render | ~15-25ms |
toImage(256x256) |
~5-15ms |
| Total per tile | ~120-145ms |
| Optimization | Latency Saved |
|---|---|
| Persistent HTTP isolate (TCP reuse) | 50-100ms |
| Chunked rendering (web) | Prevents 25ms freezes |
| Center-first loading | Perceived +500ms |
| Pre-loading Β±1 zoom | Instant zoom transitions |
Contributions welcome! Areas of focus:
- 3D building extrusion (flutter_gpu)
- Line label collision detection
- Terrain/3D globe projection
- Offline map packages
- Custom layer rendering
- Performance profiling tools
MIT License - see LICENSE for details.
- OpenFreeMap - Free vector tiles
- OpenStreetMap - Map data
- Mapbox - Vector tile specification
- Hive - Fast key-value database
- flutter_map - Alternative Flutter map library
- maplibre_gl - MapLibre GL bindings
Made with β€οΈ by the FOSM Team
High-performance maps for Flutter, without the complexity.