Tech Blog

Switching the Flutter Map Library From Google Maps to flutter_map (OpenStreetMap) — Selection Criteria for a Learning Project and OSM Tile Etiquette

by Tech Writer
Flutter flutter_map OpenStreetMap Google Maps Maps Library Selection

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_map setup 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_map from “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

ItemVersion
Flutter3.x (Material 3)
flutter_map7.0.2
latlong20.9.1
Map tiles (chosen)tile.openstreetmap.org
Map tiles (fallback)tile.openstreetmap.fr/hot
DevicePixel 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.

  1. 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”
  2. Routing and distance calculation are built-in — The Directions API draws route lines
  3. Tight coupling with Geocoding / Places — Address-string search flows are natural
  4. 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:

  1. Create a Google Cloud Platform project
  2. Enable Maps SDK for Android / iOS
  3. Issue an API key
  4. Attach a billing account (required even for free-tier use)
  5. Configure API restrictions / package-name restrictions on the key
  6. Wire it into AndroidManifest.xml / iOS AppDelegate.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,
  // ...
),
ParameterEffect
keepBuffer: 1Keep only 1 tile of off-screen cache. Conservative vs. the default 2
panBuffer: 0No prefetch on pan operations
maxNativeZoom: 18Cap 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:

AxisGoogle Maps SDKflutter_map + OSM
Initial setupAPI key issuance, billing attachment, native configOne line in pubspec.yaml
RenderingNative view (PlatformView)Pure Dart Canvas painting
MarkersMarker class (bitmap-based)Marker widget (any Widget can be placed)
Camera controlGoogleMapController.animateCamera()MapController.move()
GesturesHandled by the platformHandled by Flutter
Render performanceNative-optimizedDart side — watch out at high marker counts
Routing / GeocodingCo-resident APIsPair separately (OSRM / Nominatim etc.)
Pricing / terms$200/mo free credit + meteredPublic 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:

OptionCharacterExpected cost
Self-hosted (own tile server)OSS (OpenMapTiles / Tegola) on your own infra; OSM data downloadsServer + scaling cost
MapboxCustom styles; free up to 250k Map Loads / month$0.20 / 1000 Loads after that
MapTilerOSM-based tile service with switchable stylesFree up to 100k requests / month
Stadia MapsOSM-based, multiple styles, JA-friendlyFree up to 200k / month
Google MapsBack to the original optionMap 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 — one urlTemplate line 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.

Feel free to send a message

Please send a message if you have any technical questions, feedback, or inquiries.