Switching the Flutter Map Library From Google Maps to flutter_map (OpenStreetMap) — Selection Criteria for a Learning Project and OSM Tile Etiquette
What You’ll Learn
- The choice between “Google Maps SDK” and “
flutter_map+ OpenStreetMap tiles” when handling maps in Flutter - The four reasons Google Maps was originally planned and then dropped for this learning project
- A minimal
flutter_mapsetup that respects the OSM Tile Usage Policy (User-Agent / fallback / suppressing unnecessary requests) - How to think about the production alternatives (self-hosted / Mapbox / MapTiler etc.) when ready
Target Audience
- Flutter developers building a map-centric app, deciding between Google Maps and OSS
- People who want to take
flutter_mapfrom “it works” to “it conforms to the official tile usage policy” - Developers planning ahead at the learning-project stage for the dependency choices they’ll inherit at production
Environment
| Item | Version |
|---|---|
| Flutter | 3.x (Material 3) |
| flutter_map | 7.0.2 |
| latlong2 | 0.9.1 |
| Map tiles (chosen) | tile.openstreetmap.org |
| Map tiles (fallback) | tile.openstreetmap.fr/hot |
| Device | Pixel API 35 emulator |
Series article (in active development) — This article is part of Building Street-Level E-Scooter Sharing From Scratch to Understand It — Design, Implementation, and Operations Series. The project is ongoing; new articles and design notes are added continuously. The series motivation, technology stack, screen-ID glossary, and the full article index are collected at the link above.
Introduction
E-scooter sharing is a service where the map is the application. The home screen (S02) is essentially a full-screen map, the destination port selection (S04) is map-based, and the in-ride current location view (S10) is too — every core screen rests on a map.
I originally designed it around Google Maps SDK for Flutter (google_maps_flutter). Real shared-mobility services almost universally ship on Google Maps, and the rendering quality, route data, and POI coverage are in a class of their own.
Moving from design to implementation, I switched to flutter_map + OpenStreetMap tiles. This article records the basis for that reversal, the implementation differences, the etiquette around hitting OSM tile servers, and the path forward for production operation.
The short version: “at the learning-project stage,
flutter_map+ OSM is enough” and “for production, reconsider the options.” That’s the current judgment.
Why Google Maps Was the Original Plan
Before the part about reversing, here’s why Google Maps was the initial choice.
- It’s easy to mirror real shared-mobility services visually — Google’s tile aesthetic (road colors, POI pins) is what people are used to, so a custom UI on top looks “right”
- Routing and distance calculation are built-in — The Directions API draws route lines
- Tight coupling with Geocoding / Places — Address-string search flows are natural
- Documentation and samples are abundant — Flutter officially maintains
google_maps_flutter
In short: “If we were really shipping this for production, this is the first choice.” That’s where it started.
The Four Reasons for Reversing
At the transition from design to implementation, the reversal was driven by these four.
1. Anxiety around the pricing model — free-tier overrun risk
Google Maps Platform offers a $200/month credit equivalent, but the Mobile SDK Maps SDK shifted to “Map Loads” billing in 2018, counting every map display at startup. As trivial as this project is, you reload constantly on the emulator during development, and if a friend tries the demo, unexpected traffic is always possible.
“I checked one day and got a several-hundred-dollar bill” is a real-world failure mode. Putting metered billing risk on a personal learning project is just not worth it.
2. The API-key / billing setup is friction
Using Google Maps SDK requires at minimum:
- Create a Google Cloud Platform project
- Enable Maps SDK for Android / iOS
- Issue an API key
- Attach a billing account (required even for free-tier use)
- Configure API restrictions / package-name restrictions on the key
- Wire it into
AndroidManifest.xml/ iOSAppDelegate.swift
For a phase where the goal is “designing while feeling the implementation,” this is too much noise. OSS tiles let you add one line to pubspec.yaml and have a map on screen — that immediacy wins on its loop with the design cycle.
3. Free, OSS-based starting matches the project’s identity
The overall series identity is “speculate → design → build a street-level e-scooter sharing service from zero to understand it.” The hard parts aren’t map rendering quality. They’re the structure of port-selection UI, the destination-reservation flow, state gates, IoT integration — “the bones of the service.”
The map is “render something” rather than “achieve commercial-grade quality.” Starting fast with OSS and spending time on the structural design is the higher-learning-yield path.
4. License / attribution simplicity
Using Google Maps brings constraints around credit display, screenshot reuse rules, and map-image redistribution. For a project that writes about its own UI in blog posts (this one included), constantly verifying image-use rules is a quietly heavy load.
OpenStreetMap operates under ODbL (Open Database License) and CC BY-SA flavors. ”© OpenStreetMap contributors” attribution is the headline requirement, and that’s a clean fit for blog screenshots.
Chosen Stack: flutter_map + OpenStreetMap Tiles
flutter_map ports the spirit of Leaflet.js to Flutter. Tile servers are swappable via a URL template, which means switching off OSM later (to Mapbox / MapTiler / self-hosted) is built into the abstraction. That was the decisive factor.
# pubspec.yaml (excerpt)
dependencies:
flutter_map: ^7.0.2
latlong2: ^0.9.1
A minimal FlutterMap widget:
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _defaultCenter,
initialZoom: 14.5,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
fallbackUrl:
'https://tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
keepBuffer: 1,
panBuffer: 0,
maxNativeZoom: 18,
userAgentPackageName: 'com.y104autumn.e_scooter_sharing_app',
),
MarkerLayer(markers: _portMarkers),
],
)
This TileLayer has several settings that come from being mindful of the OSM Tile Usage Policy. They’re explained one by one below.
Etiquette When Hitting OSM’s Public Tile Servers
The OpenStreetMap Tile Usage Policy governs use of tile.openstreetmap.org operated by the OSM Foundation. In summary:
- Production use is discouraged (the infrastructure is donation-funded)
- Send an identifiable User-Agent / HTTP Referer
- Cache aggressively and don’t send unnecessary requests
- Constrain zoom levels and viewport to avoid over-prefetching
- Provide attribution to OSM in any usage
What’s reflected in the implementation, in order:
1. Identify the app via User-Agent
TileLayer(
// ...
userAgentPackageName: 'com.y104autumn.e_scooter_sharing_app',
),
flutter_map’s userAgentPackageName is embedded into the HTTP request’s User-Agent. The point is letting OSM identify and contact a problematic client if needed. Without it, the OSS tile server may block anonymous access.
2. Provide a fallback tile
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
fallbackUrl:
'https://tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
// ...
),
fallbackUrl is used when the main tile server returns 4xx / 5xx. Pairing the OSM France HOT (Humanitarian OpenStreetMap Team) style means a transient outage on one server doesn’t blank the map.
But the fallback is also run by a separate volunteer org. The right mental model is not “two layers of safety,” it’s “neither should be used in production.”
3. Suppress prefetching
TileLayer(
keepBuffer: 1,
panBuffer: 0,
maxNativeZoom: 18,
// ...
),
| Parameter | Effect |
|---|---|
keepBuffer: 1 | Keep only 1 tile of off-screen cache. Conservative vs. the default 2 |
panBuffer: 0 | No prefetch on pan operations |
maxNativeZoom: 18 | Cap requested zoom at 18 (anything beyond is client-side upscaling) |
“Don’t go fetch tiles you don’t need” is the single biggest courtesy when consuming OSM tiles. Defaults will fire dozens of tile requests per pan — explicitly clamp them.
4. Don’t hit your own API mid-drag, either
Separately from the tile server, the Spring Boot API also has to be asked for port data. Calling /api/v1/ports?bbox=... every time the map center moves overloads that side too.
options: MapOptions(
onMapEvent: (event) {
final camera = event.camera;
_cameraCenter = camera.center;
_cameraZoom = camera.zoom;
// Don't fire APIs mid-drag — only after the map settles.
if (event is MapEventMoveEnd) {
_scheduleRefreshPorts();
}
},
),
MapEventMoveEnd triggers the port refresh after the camera settles. This is for the in-house API, not OSM tiles, but it’s the same family of concern: “map-based UI very easily over-fires requests without deliberate suppression.”
5. Attribution
// Example: pinned bottom of the screen
const Padding(
padding: EdgeInsets.all(4),
child: Text(
'© OpenStreetMap contributors',
style: TextStyle(fontSize: 10, color: Colors.black54),
),
)
The OSM license (ODbL) requires attribution at the point of use. flutter_map ships a RichAttributionWidget and friends for this — using those is cleaner.
Implementation Differences vs. Google Maps
Hands-on differences vs. google_maps_flutter:
| Axis | Google Maps SDK | flutter_map + OSM |
|---|---|---|
| Initial setup | API key issuance, billing attachment, native config | One line in pubspec.yaml |
| Rendering | Native view (PlatformView) | Pure Dart Canvas painting |
| Markers | Marker class (bitmap-based) | Marker widget (any Widget can be placed) |
| Camera control | GoogleMapController.animateCamera() | MapController.move() |
| Gestures | Handled by the platform | Handled by Flutter |
| Render performance | Native-optimized | Dart side — watch out at high marker counts |
| Routing / Geocoding | Co-resident APIs | Pair separately (OSRM / Nominatim etc.) |
| Pricing / terms | $200/mo free credit + metered | Public OSM tiles: free but not for production |
What worked best in practice was “markers are arbitrary widgets.” Google Maps’ Marker swaps bitmap assets; flutter_map accepts a Container, a Card, anything — so state-based pin variants (available / full) stay in UI code.
The weakness is render performance with many markers. Hundreds of pins on screen surface Canvas painting cost. At the density of a sharing service (10–100 per screen) it’s a non-issue.
When Moving to Production: What Would I Choose?
The OSM public tile server is “not for production” by policy. Moving to production means revisiting the choice. Current candidate set:
| Option | Character | Expected cost |
|---|---|---|
| Self-hosted (own tile server) | OSS (OpenMapTiles / Tegola) on your own infra; OSM data downloads | Server + scaling cost |
| Mapbox | Custom styles; free up to 250k Map Loads / month | $0.20 / 1000 Loads after that |
| MapTiler | OSM-based tile service with switchable styles | Free up to 100k requests / month |
| Stadia Maps | OSM-based, multiple styles, JA-friendly | Free up to 200k / month |
| Google Maps | Back to the original option | Map Loads metering |
The advantage of having chosen flutter_map is that swapping TileLayer.urlTemplate is enough to move between any of those. The flexibility to decide later, based on real traffic projections, is preserved.
// Pseudo-code for switching to Mapbox
TileLayer(
urlTemplate: 'https://api.mapbox.com/styles/v1/{username}/{style_id}/tiles/{z}/{x}/{y}?access_token={access_token}',
// ...
),
So the current stack is positioned as “OSS for the learning phase, without narrowing the options for production.”
Takeaways
- Map library selection is a “billing and policy risk” question before it’s a “render quality” question — removing metered-billing risk from a learning project is a huge psychological win
flutter_map’s abstraction is the insurance policy for production switching — oneurlTemplateline moves between any of several tile vendors- Hitting OSM’s public tiles requires settings that respect the Tile Usage Policy — User-Agent / prefetch suppression / fallback / attribution. Defaults silently exploit donation-funded infrastructure
- Markers being “any Widget” simplifies UI work — pin-style variants by state stay in Dart code
The map is the face of the service, but the selection axis at the learning phase doesn’t have to be the production axis. When this project moves to production, expect a follow-up comparing Mapbox / MapTiler / self-hosted.