Flutter の地図ライブラリを Google Maps から flutter_map (OpenStreetMap) に変更した話 — 学習プロジェクトでの選定基準とOSMタイル利用マナー
この記事でわかること
- Flutter で地図を扱う時の「Google Maps SDK」と「
flutter_map+ OpenStreetMap タイル」の選択肢の違い - 学習プロジェクトで Google Maps を 当初予定していたが撤回した 4つの理由
flutter_mapで OSM タイルを使う時の最小実装と、OSM Tile Usage Policy に沿った設定(User-Agent / フォールバック / 不要リクエスト抑制)- 本番運用に切り替えるなら何を選ぶかの判断軸(セルフホスト / Mapbox / MapTiler 等)
対象読者
- Flutter で地図ベースのアプリを作ろうとしていて、Google Maps か OSS かで迷っている方
flutter_mapのタイル設定を「とりあえず動く」から「公式の利用規約に沿った形」へ整えたい方- 学習プロジェクトの段階で、本番化を見据えた依存先の決め方を整理したい方
動作環境
| 項目 | バージョン |
|---|---|
| Flutter | 3.x(Material 3) |
| flutter_map | 7.0.2 |
| latlong2 | 0.9.1 |
| 地図タイル(採用) | tile.openstreetmap.org |
| 地図タイル(フォールバック) | tile.openstreetmap.fr/hot |
| 端末 | Pixel API 35 エミュレーター |
シリーズ記事(開発進行中) — この記事は 街中の電動キックボードシェアリングを自作して理解する — 設計・実装・運用の記録シリーズ の一部です。プロジェクトは現在も継続中で、新しい記事や設計判断の追記が随時行われます。プロジェクト全体の動機・採用技術・画面IDなどの用語表・他記事への索引はリンク先にまとめています。
はじめに
電動キックボードシェアリングは、地図がアプリの中心 であるサービスです。ホーム画面(S02)はそもそも全画面が地図ですし、目的地ポート選択(S04)も、ライド中の現在地表示(S10)も、すべて地図ベースで成立します。
当初は Google Maps SDK for Flutter (google_maps_flutter) を使う前提で設計を進めていました。シェアリングモビリティ系の実サービスはほぼ Google Maps を採用しており、レンダリング品質・経路情報・POI 情報の豊富さは別格です。
しかし、設計から実装フェーズに移る段階で flutter_map + OpenStreetMap タイル へ変更しました。本記事はその撤回判断の根拠と、実装上の違い、OSM タイルを叩く時の作法、本番運用に切り替える時の選択肢の整理です。
結論を先に書くと、「学習プロジェクトの段階では
flutter_map+ OSM で十分」「ただし本番運用なら別の選択肢を検討する」が現時点の判断です。
Google Maps を当初予定していた理由
撤回した話の前に、最初なぜ Google Maps を選んでいたかを残しておきます。
- 本物のシェアリングサービスとUIの参考にしやすい — Google Maps タイル特有のスタイル(道路の色、POI ピン)を見慣れているので、自作 UI が「それっぽく」見える
- ポイント間ルーティング・距離計算が組み込み — Directions API でルート線が引ける
- Geocoding / Places API との一体感 — 住所文字列ベースで検索する画面と相性が良い
- ドキュメントとサンプルが豊富 — Flutter 公式が
google_maps_flutterをメンテナンスしている
要するに「本番で実際にやるならまずこれ」を最初の選択肢に置いていました。
撤回した4つの理由
設計から実装に移るタイミングで、以下を理由に撤回しました。
1. 課金体系の不安 — 無料枠超過と課金のリスク
Google Maps Platform は 月 $200 相当の無料クレジット がありますが、Mobile SDK の Maps SDK 単体は2018年以降「Map Loads」ベースの課金体系に変わり、起動時のマップ表示ごとにカウント されます。学習プロジェクトとはいえ、開発中はエミュレーターで何度もリロードしますし、デモのために知人に触ってもらえば、想定外のトラフィックが発生する可能性は常にあります。
「気付いたら数百ドル請求された」というケースは現実に起きうるので、学習目的の個人プロジェクトに従量課金リスクを背負わせるのは割が合わない と判断しました。
2. API キー・課金設定のセットアップが面倒
Google Maps SDK を使うには、最低でも以下のステップが必要です。
- Google Cloud Platform プロジェクト作成
- Maps SDK for Android / iOS を有効化
- API キーの発行
- 課金アカウントの紐づけ(無料枠を使う場合でも必須)
- キーの API 制限・パッケージ名制限の設定
- Android
AndroidManifest.xml/ iOSAppDelegate.swiftへの組み込み
これは「設計を考えながら実装の手応えを得る」フェーズではノイズが大きすぎます。OSS タイルを使えば pubspec.yaml に1行追加するだけで地図が出る という即時性が、設計サイクルとの相性で勝りました。
3. OSS・無料で始められる気軽さが学習プロジェクトに合う
シリーズ全体の思想として、「街中の電動キックボードシェアリングを自分なりに考察→設計→製造して理解する」 がプロジェクトの目的です。詰まる場所は地図のレンダリング品質ではなく、ポート選択 UI の構造、目的地予約のフロー、状態ガード、IoT 連携の仕組み、といった「サービスの骨格」のはず。
地図は 「とりあえず描画できればいい」 であって、商用 SDK の最高品質は要りません。OSS で素早く始められて、サービスの本質的な設計に時間を使える方が学習効果が高い、と割り切りました。
4. ライセンス・表示規約の軽さ
Google Maps を使う場合、利用規約として クレジット表示・スクリーンショット利用制限・地図画像の二次利用ルール に縛られます。プロジェクト記事(このブログ含む)で地図画面のキャプチャを貼る運用を考えると、画像表示についての規約をその都度確認する負荷 が地味に重い。
OpenStreetMap は ODbL (Open Database License) と CC BY-SA 系で運用されており、「© OpenStreetMap contributors」のクレジット表記 さえ守れば、ブログ記事への画面キャプチャ掲載が気楽です。
採用構成: flutter_map + OpenStreetMap タイル
flutter_map は、Leaflet.js 系の思想を Flutter に持ち込んだ OSS の地図ライブラリです。タイルサーバを URL テンプレートで差し替えできるので、OSM 以外(Mapbox / MapTiler / 自前サーバ)にも乗り換え可能 な抽象が組み込まれているのが選定の決め手でした。
# pubspec.yaml(抜粋)
dependencies:
flutter_map: ^7.0.2
latlong2: ^0.9.1
最小の FlutterMap ウィジェット:
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),
],
)
この TileLayer には OSM Tile Usage Policy を意識した設定 が複数入っています。次節で順に説明します。
OSM 公式タイルを叩く時の作法
OpenStreetMap Tile Usage Policy は、OSM Foundation が運用する tile.openstreetmap.org の利用ルールをまとめたものです。要約すると:
- 本番サービスでの利用は推奨されない(負荷分散インフラが寄付ベースのため)
- 識別可能な User-Agent / HTTP Referer を必ず付ける
- キャッシュを活用 し、不要なリクエストを送らない
- ズームレベルや表示範囲を制限し、プリフェッチ過多 を避ける
- 利用にあたっては OSM への帰属表示(attribution)を行う
実装側で対応しているポイントを順に見ます。
1. User-Agent でアプリを識別可能にする
TileLayer(
// ...
userAgentPackageName: 'com.y104autumn.e_scooter_sharing_app',
),
flutter_map の userAgentPackageName は HTTP リクエストの User-Agent に組み込まれます。OSM 側が問題のあるクライアントを特定できるようにする ための設定で、設定しないと OSS タイルサーバが匿名アクセスをブロックする可能性があります。
2. フォールバックタイルを用意する
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
fallbackUrl:
'https://tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
// ...
),
fallbackUrl は、メインのタイルサーバから 4xx / 5xx が返った時に代替で使われます。OSM France の HOT(Humanitarian OpenStreetMap Team)スタイルを併用することで、片方のサーバが一時的に不調でも地図が空白にならない構成になります。
ただしフォールバックも別組織が運用するボランタリーなインフラなので、「2系統あれば安心」ではなく 「両方とも本番では使うべきではない」 が前提です。
3. プリフェッチを抑制する
TileLayer(
keepBuffer: 1,
panBuffer: 0,
maxNativeZoom: 18,
// ...
),
| パラメータ | 効果 |
|---|---|
keepBuffer: 1 | 画面外1タイル分だけキャッシュ保持。デフォルト2より控えめ |
panBuffer: 0 | パン操作時の先読みプリフェッチを行わない |
maxNativeZoom: 18 | サーバへリクエストする最大ズームを18に制限(それ以上はクライアント側で拡大表示) |
「不要なタイルを取りに行かない」が OSM タイル使用時の最大のマナー です。デフォルトのまま使うとパン1回で十数枚のタイルが連続リクエストされるので、明示的に絞り込みます。
4. ドラッグ中はAPIを叩かない(地図側でなく自前API)
地図サーバへのリクエストとは別に、自前 API(Spring Boot 側)にもポート情報を取りに行く 必要があります。地図の中心が動くたびに /api/v1/ports?bbox=... を叩くと、こちらも過剰負荷になります。
options: MapOptions(
onMapEvent: (event) {
final camera = event.camera;
_cameraCenter = camera.center;
_cameraZoom = camera.zoom;
// ドラッグ中ではなく、停止後にだけAPIを呼ぶ。
if (event is MapEventMoveEnd) {
_scheduleRefreshPorts();
}
},
),
MapEventMoveEnd で 停止検知後にだけ ポート再取得をスケジュールしています。これは OSM タイル側ではなく自前 API への配慮ですが、「地図ベース UI は意識的にリクエスト抑制しないと簡単に過剰負荷になる」という同じ系統の話です。
5. クレジット表示(attribution)
// 例: 画面下部に固定表示
const Padding(
padding: EdgeInsets.all(4),
child: Text(
'© OpenStreetMap contributors',
style: TextStyle(fontSize: 10, color: Colors.black54),
),
)
OSM ライセンス(ODbL)は 二次利用先での帰属表示 を要求します。flutter_map には RichAttributionWidget 等の専用ウィジェットも用意されているので、それを使う方が体裁が整います。
Google Maps との実装面の違い
実装してみて感じた、google_maps_flutter との具体的な違いです。
| 観点 | Google Maps SDK | flutter_map + OSM |
|---|---|---|
| 初期セットアップ | API キー発行・課金紐づけ・ネイティブ設定 | pubspec.yaml に1行 |
| 地図描画 | ネイティブビュー(PlatformView) | Pure Dart の Canvas 描画 |
| マーカー | Marker クラス(ビットマップ系) | Marker ウィジェット(任意の Widget を配置可能) |
| カメラ制御 | GoogleMapController.animateCamera() | MapController.move() |
| ジェスチャー | プラットフォーム側で処理 | Flutter 側で処理 |
| 描画性能 | ネイティブ最適化済み | Dart 側なので大量マーカー時に注意 |
| Routing / Geocoding | API が同居 | 別途 API(OSRM / Nominatim 等)を組む |
| 規約・課金 | 月 $200 無料枠 + 従量課金 | 公式 OSM タイル: 無料だが本番非推奨 |
特に効いたのは 「マーカーが任意の Widget」 という点です。Google Maps の Marker はビットマップアセットを差し替える形ですが、flutter_map では Container でも Card でも何でも置けるので、ポート状態(空きあり/満杯)に応じた色や数字の出し分けが UI コードで完結 します。
逆に弱いのは 大量マーカー時の描画性能 で、画面内に何百件もピンを置くと Dart の Canvas 描画コストが効いてきます。シェアリング規模のポート数(10〜100程度/画面)なら問題なし。
本番運用に切り替えるなら何を選ぶか
OSM 公式タイルは「本番サービス利用は推奨されない」のが大前提なので、本番化を考えるなら別の選択肢を検討する必要があります。現時点で整理している候補:
| 選択肢 | 特徴 | 想定コスト |
|---|---|---|
| セルフホスト(自前タイルサーバ) | OSS(OpenMapTiles / Tegola)を自前運用。地図データは OSM ダウンロードから | サーバ運用コスト。トラフィック増にスケーリングが必要 |
| Mapbox | カスタムスタイル可能。月25万 Map Loads までは無料 | 超過後 $0.20〜/1000 Loads |
| MapTiler | OSM ベースのタイル提供。スタイル切替可能 | 月10万リクエストまで無料 |
| Stadia Maps | OSM ベースで複数スタイル提供。日本向けに使いやすい | 月20万まで無料 |
| Google Maps | 最初の選択肢に戻る | Map Loads 課金 |
flutter_map を選んでおく利点は、TileLayer.urlTemplate を差し替えるだけで上記のどれにでも乗り換えられる こと。本番化判断のタイミングで、トラフィック試算とコストを見て決められる柔軟性が残ります。
// Mapbox に切り替える例(pseudo)
TileLayer(
urlTemplate: 'https://api.mapbox.com/styles/v1/{username}/{style_id}/tiles/{z}/{x}/{y}?access_token={access_token}',
// ...
),
つまり今の構成は 「学習段階は OSS 無料で進めつつ、本番化時の選択肢を狭めない」 ためのレイヤリングという位置付けです。
学び
- 地図ライブラリ選定は「描画品質」より「課金と規約のリスク評価」が先 という気づき。学習プロジェクトに従量課金リスクを持ち込まないだけで、心理的負担が大幅に下がる
flutter_mapの抽象は本番乗り換えの保険になる —urlTemplate1行で複数のタイル提供業者に切り替えられる- OSM 公式タイルを叩く時は Tile Usage Policy に沿った設定をする — User-Agent / プリフェッチ抑制 / フォールバック / 帰属表示。デフォルトのまま使うと OSM 側の善意で動いているサーバに負荷をかけることになる
- マーカーが「任意の Widget」になることでUI実装が楽になる — 状態によるピンの見た目変化が Dart コードだけで書ける
地図はサービスの顔ですが、学習段階の選定基準は本番運用とは別軸で考えていい というのが今回の判断のコアでした。本番運用に進むタイミングで、改めて Mapbox / MapTiler / セルフホストの比較記事を書く予定です。