เปลี่ยน Map Library ของ Flutter จาก Google Maps ไป flutter_map (OpenStreetMap) — เกณฑ์การเลือกสำหรับโปรเจกต์เรียนรู้และมารยาทการใช้ OSM Tile
สิ่งที่คุณจะได้เรียนรู้
- ทางเลือกระหว่าง “Google Maps SDK” กับ “
flutter_map+ OpenStreetMap tile” เมื่อจัดการแผนที่ใน Flutter - 4 เหตุผลที่ Google Maps เคยอยู่ในแผนแต่ถูกถอน สำหรับโปรเจกต์เรียนรู้นี้
- การ setup
flutter_mapขั้นต่ำที่เคารพ OSM Tile Usage Policy (User-Agent / fallback / กดปริมาณ request ที่ไม่จำเป็น) - วิธีคิดทางเลือก production (self-hosted / Mapbox / MapTiler ฯลฯ) เมื่อพร้อม
กลุ่มเป้าหมาย
- นักพัฒนา Flutter ที่สร้างแอปที่แผนที่เป็นแกนหลัก กำลังเลือกระหว่าง Google Maps กับ OSS
- ผู้ที่ต้องการพา
flutter_mapจาก “ทำงานได้” ไป “ทำงานตาม official tile usage policy” - นักพัฒนาที่อยู่ในช่วงโปรเจกต์เรียนรู้ และอยากวางแผน dependency ของ production ล่วงหน้า
สภาพแวดล้อม
| รายการ | เวอร์ชัน |
|---|---|
| Flutter | 3.x (Material 3) |
| flutter_map | 7.0.2 |
| latlong2 | 0.9.1 |
| Map tile (เลือกใช้) | tile.openstreetmap.org |
| Map tile (fallback) | tile.openstreetmap.fr/hot |
| อุปกรณ์ | Pixel API 35 emulator |
บทความในชุด (อยู่ระหว่างการพัฒนา) — บทความนี้เป็นส่วนหนึ่งของ สร้าง E-Scooter Sharing ระดับท้องถนนตั้งแต่ศูนย์เพื่อทำความเข้าใจ — ชุดบันทึก Design, Implementation, และ Operations โปรเจกต์ยังดำเนินอยู่และมีการเพิ่มบทความและบันทึก design ใหม่อย่างต่อเนื่อง แรงจูงใจของชุด, technology stack, อภิธานศัพท์ screen ID, และดัชนีบทความถูกรวบรวมไว้ที่ลิงก์ด้านบน
บทนำ
E-scooter sharing เป็นบริการที่ แผนที่คือตัวแอป หน้าหลัก (S02) คือแผนที่เต็มจอ การเลือกพอร์ตปลายทาง (S04) ก็ใช้แผนที่ และ ride in progress (S10) ก็เช่นกัน — ทุกหน้าจอแกนกลางอยู่บนแผนที่
ตอนแรก ฉัน design รอบ Google Maps SDK for Flutter (google_maps_flutter) บริการเช่ารถระดับโลกแทบทั้งหมดใช้ Google Maps และคุณภาพการ render, ข้อมูล route, ครอบคลุม POI อยู่อีกระดับ
เมื่อย้ายจาก design ไป implementation ฉันเปลี่ยนเป็น flutter_map + OpenStreetMap tile บทความนี้บันทึกพื้นฐานของการถอนการตัดสินใจ ความต่างเชิง implementation มารยาทเมื่อยิง OSM tile server และเส้นทางต่อไปสำหรับ production operation
สรุปสั้น: “ในช่วง learning project,
flutter_map+ OSM พอแล้ว” และ “สำหรับ production ต้องพิจารณาทางเลือกใหม่” คือคำตัดสินตอนนี้
ทำไม Google Maps เป็นแผนเริ่มต้น
ก่อนพูดเรื่องการถอน นี่คือเหตุผลที่ Google Maps เป็นตัวเลือกเริ่มแรก
- เลียนแบบบริการเช่ารถจริงในแง่ภาพได้ง่าย — สี tile ของ Google (สีถนน, POI pin) คือสิ่งที่คนคุ้นเคย ทำให้ UI ของเรา “ดูถูกต้อง”
- Routing และคำนวณระยะทาง built-in — Directions API ลาก route line ได้
- เชื่อมโยงกับ Geocoding / Places แน่น — flow ค้นด้วย address string เป็นธรรมชาติ
- เอกสารและตัวอย่างเพียบ — Flutter ดูแล
google_maps_flutterอย่างเป็นทางการ
สรุปสั้น: “ถ้าเรา ship จริงสำหรับ production นี่คือทางเลือกแรก” — เริ่มจากตรงนั้น
4 เหตุผลที่ถอนการตัดสินใจ
ในจุดเปลี่ยนจาก design ไป implementation การถอนการตัดสินใจเกิดจาก 4 ข้อนี้
1. ความกังวลเรื่อง pricing model — ความเสี่ยงเกิน free tier
Google Maps Platform ให้ เครดิตเทียบเท่า $200/เดือน แต่ Mobile SDK Maps SDK เปลี่ยนเป็น billing แบบ “Map Loads” ในปี 2018 ซึ่ง นับทุกการแสดงแผนที่ตอน startup แม้โปรเจกต์นี้จะเล็ก แต่ระหว่างพัฒนา reload บน emulator บ่อย และถ้าเพื่อนลองเดโม่ การมี traffic เกินคาดเป็นไปได้เสมอ
“วันหนึ่งเช็กแล้วมาบิลหลักร้อย dollars” คือ failure mode ที่เกิดจริง การวาง metered billing risk บนโปรเจกต์เรียนรู้ส่วนตัวไม่คุ้ม
2. การ setup API-key / billing เป็น friction
การใช้ Google Maps SDK ต้องอย่างน้อย:
- สร้าง Google Cloud Platform project
- Enable Maps SDK for Android / iOS
- ออก API key
- ผูก billing account (ต้องมีแม้ใช้ free tier)
- ตั้ง API restriction / package-name restriction บน key
- ต่อกับ
AndroidManifest.xml/ iOSAppDelegate.swift
สำหรับ phase ที่เป้าหมายคือ “design ขณะรู้สึก implementation” สิ่งนี้คือ noise มากเกินไป OSS tile ให้เพิ่ม 1 บรรทัดใน pubspec.yaml แล้วได้แผนที่บนจอ — ความรวดเร็วนั้นชนะใน loop กับ design cycle
3. การเริ่มแบบ OSS ฟรี เข้ากับ identity ของโปรเจกต์
Identity ทั้งชุดคือ “เก็งร่าง → design → สร้างบริการ e-scooter sharing ระดับท้องถนนจากศูนย์เพื่อทำความเข้าใจ” ส่วนยากไม่ใช่คุณภาพการ render แผนที่ แต่คือโครงสร้างของ port-selection UI, flow การจอง destination, state gate, IoT integration — “กระดูกของบริการ”
แผนที่คือ “render อะไรก็ได้” ไม่ใช่ “บรรลุคุณภาพระดับเชิงพาณิชย์” เริ่มเร็วด้วย OSS และใช้เวลากับการ design เชิงโครงสร้างจึงเป็นเส้นทางผลตอบแทนการเรียนรู้สูงกว่า
4. ความเรียบง่ายของ license / attribution
การใช้ Google Maps มีข้อจำกัดรอบ การแสดง credit, กฎการนำ screenshot ไปใช้ใหม่, และการเผยแพร่ภาพแผนที่ซ้ำ สำหรับโปรเจกต์ที่เขียนบล็อกเกี่ยวกับ UI ตัวเอง (รวมบทความนี้) การยืนยันกฎการใช้ภาพอย่างต่อเนื่อง เป็นภาระเงียบ ๆ ที่หนัก
OpenStreetMap ดำเนินงานภายใต้ ODbL (Open Database License) และ CC BY-SA flavor การให้ attribution ”© OpenStreetMap contributors” เป็นข้อกำหนดหลัก ซึ่งเข้ากับ screenshot บล็อกอย่างเรียบร้อย
Stack ที่เลือก: flutter_map + OpenStreetMap Tile
flutter_map นำจิตวิญญาณของ Leaflet.js มาสู่ Flutter Tile server สลับได้ผ่าน URL template หมายความว่า การย้ายออกจาก OSM ภายหลัง (ไป Mapbox / MapTiler / self-hosted) ฝังอยู่ใน abstraction นั่นคือปัจจัยตัดสินใจ
# pubspec.yaml (excerpt)
dependencies:
flutter_map: ^7.0.2
latlong2: ^0.9.1
Widget 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 นี้มี setting หลายตัวที่มาจากความใส่ใจต่อ OSM Tile Usage Policy อธิบายทีละข้อด้านล่าง
มารยาทเมื่อยิง OSM Public Tile Server
OpenStreetMap Tile Usage Policy กำกับการใช้ tile.openstreetmap.org ที่ OSM Foundation ดำเนินงาน สรุป:
- ไม่แนะนำสำหรับ production (infrastructure ได้รับเงินสนับสนุนจาก donation)
- ส่ง User-Agent / HTTP Referer ที่ระบุตัวตนได้
- Cache ให้มาก และอย่าส่ง request ที่ไม่จำเป็น
- จำกัด zoom level และ viewport เพื่อ หลีกเลี่ยง over-prefetching
- ให้ attribution กับ OSM ในการใช้
สิ่งที่สะท้อนใน implementation ตามลำดับ:
1. ระบุแอปผ่าน User-Agent
TileLayer(
// ...
userAgentPackageName: 'com.y104autumn.e_scooter_sharing_app',
),
userAgentPackageName ของ flutter_map ถูกฝังใน User-Agent ของ HTTP request จุดประสงค์คือ ให้ OSM ระบุและติดต่อ client ที่มีปัญหา ได้เมื่อจำเป็น หากไม่ตั้ง OSS tile server อาจ block anonymous access
2. ใส่ fallback tile
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
fallbackUrl:
'https://tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
// ...
),
fallbackUrl ถูกใช้เมื่อ tile server หลัก return 4xx / 5xx การจับคู่กับ style OSM France HOT (Humanitarian OpenStreetMap Team) ทำให้ outage ชั่วคราวของเซิร์ฟเวอร์หนึ่งไม่ทำให้แผนที่เปล่า
แต่ fallback ก็เป็น volunteer org แยกที่รัน mental model ที่ถูกต้องไม่ใช่ “ความปลอดภัยสองชั้น” แต่คือ “ทั้งสองไม่ควรใช้ใน production”
3. กด prefetch
TileLayer(
keepBuffer: 1,
panBuffer: 0,
maxNativeZoom: 18,
// ...
),
| Parameter | ผล |
|---|---|
keepBuffer: 1 | เก็บ off-screen cache เพียง 1 tile อนุรักษ์มากกว่า default 2 |
panBuffer: 0 | ไม่ prefetch ตอน pan |
maxNativeZoom: 18 | จำกัด zoom ที่ขอเป็น 18 (มากกว่านั้น upscale ฝั่ง client) |
“ไม่ไปดึง tile ที่ไม่ต้องการ” คือมารยาทใหญ่ที่สุดเมื่อใช้ OSM tile Default จะยิง tile request หลายสิบครั้งต่อ pan — จำกัดอย่างชัดเจน
4. อย่ายิง API ของตัวเองระหว่าง drag เช่นกัน
แยกจาก tile server Spring Boot API ก็ต้องถูกขอข้อมูลพอร์ต การเรียก /api/v1/ports?bbox=... ทุกครั้งที่ map center ขยับ overload ฝั่งนั้นด้วย
options: MapOptions(
onMapEvent: (event) {
final camera = event.camera;
_cameraCenter = camera.center;
_cameraZoom = camera.zoom;
// ไม่ยิง API ระหว่าง drag — ยิงเฉพาะหลังแผนที่หยุด
if (event is MapEventMoveEnd) {
_scheduleRefreshPorts();
}
},
),
MapEventMoveEnd trigger port refresh หลังกล้องหยุด สิ่งนี้สำหรับ in-house API ไม่ใช่ OSM tile แต่เป็น family เดียวกันของความกังวล: “UI ตามแผนที่ยิง request เกินจริงได้ง่ายโดยไม่ตั้งใจกด”
5. Attribution
// ตัวอย่าง: ปักไว้ที่ก้นจอ
const Padding(
padding: EdgeInsets.all(4),
child: Text(
'© OpenStreetMap contributors',
style: TextStyle(fontSize: 10, color: Colors.black54),
),
)
License ของ OSM (ODbL) ต้องการ attribution ที่จุดการใช้งาน flutter_map มี RichAttributionWidget และเพื่อนสำหรับสิ่งนี้ — ใช้ตัวเหล่านั้นจะเรียบร้อยกว่า
ความต่างเชิง Implementation เทียบกับ Google Maps
ความต่างจากประสบการณ์จริงเทียบกับ google_maps_flutter:
| แกน | Google Maps SDK | flutter_map + OSM |
|---|---|---|
| Initial setup | ออก API key, ผูก billing, config native | 1 บรรทัดใน pubspec.yaml |
| Rendering | Native view (PlatformView) | Pure Dart Canvas painting |
| Marker | คลาส Marker (เน้น bitmap) | Widget Marker (วาง Widget อะไรก็ได้) |
| Camera control | GoogleMapController.animateCamera() | MapController.move() |
| Gesture | จัดการโดย platform | จัดการโดย Flutter |
| Performance | Native-optimized | ฝั่ง Dart — ระวังที่ marker จำนวนมาก |
| Routing / Geocoding | API อยู่ร่วม | จับคู่แยก (OSRM / Nominatim ฯลฯ) |
| Pricing / terms | $200/mo free credit + metered | OSM public tile: ฟรีแต่ไม่สำหรับ production |
สิ่งที่ work ที่สุดในทางปฏิบัติคือ “marker เป็น Widget อะไรก็ได้” Marker ของ Google Maps สลับ bitmap asset; flutter_map รับ Container, Card, อะไรก็ได้ — ดังนั้น variant ของ pin ตามสถานะ (ว่าง / เต็ม) อยู่ใน UI code
จุดอ่อนคือ performance การ render ที่ marker จำนวนมาก Pin หลายร้อยบนจอเปิดเผยต้นทุน Canvas painting ที่ความหนาแน่นของบริการเช่ารถ (10–100 ต่อจอ) ไม่เป็นปัญหา
เมื่อย้ายไป Production: จะเลือกอะไร?
OSM public tile server คือ “ไม่สำหรับ production” ตามนโยบาย การย้ายไป production หมายถึงทบทวนการเลือกใหม่ ชุดผู้สมัครปัจจุบัน:
| ทางเลือก | ลักษณะ | ต้นทุนที่คาดการณ์ |
|---|---|---|
| Self-hosted (tile server ของตนเอง) | OSS (OpenMapTiles / Tegola) บน infra ของตน; ดาวน์โหลดข้อมูล OSM | Server + scaling cost |
| Mapbox | Custom style; ฟรีถึง 250k Map Loads / เดือน | $0.20 / 1000 Loads ที่เกิน |
| MapTiler | Tile service ฐาน OSM ที่ switch style ได้ | ฟรีถึง 100k request / เดือน |
| Stadia Maps | ฐาน OSM, style หลายแบบ, ใช้ในญี่ปุ่นสะดวก | ฟรีถึง 200k / เดือน |
| Google Maps | กลับมาที่ตัวเลือกเดิม | Map Loads metering |
ข้อได้เปรียบของการเลือก flutter_map คือ สลับ TileLayer.urlTemplate พอที่จะย้ายไปยังทางเลือกใดก็ได้ข้างต้น ความยืดหยุ่นในการตัดสินใจภายหลังตามการคาดการณ์ traffic จริงยังคงอยู่
// Pseudo-code สลับไป Mapbox
TileLayer(
urlTemplate: 'https://api.mapbox.com/styles/v1/{username}/{style_id}/tiles/{z}/{x}/{y}?access_token={access_token}',
// ...
),
ดังนั้น stack ปัจจุบันถูกวางเป็น “OSS ในช่วง learning โดยไม่บีบทางเลือกของ production”
บทเรียน
- การเลือก map library คือคำถาม “ความเสี่ยง billing และ policy” ก่อนคำถาม “คุณภาพการ render” — การลบ metered billing risk จากโปรเจกต์เรียนรู้คือชัยชนะทางจิตวิทยา
- Abstraction ของ
flutter_mapคือกรมธรรม์ประกันสำหรับ production switching — 1 บรรทัดurlTemplateย้ายระหว่าง tile vendor หลายตัว - การยิง OSM public tile ต้องการ setting ที่เคารพ Tile Usage Policy — User-Agent / prefetch suppression / fallback / attribution Default ขูดรีด infrastructure ที่ได้รับ donation อย่างเงียบ ๆ
- Marker เป็น “Widget อะไรก็ได้” ทำให้งาน UI เรียบง่ายขึ้น — variant style ตามสถานะอยู่ใน Dart code
แผนที่คือใบหน้าของบริการ แต่ แกนการเลือกใน learning phase ไม่จำเป็นต้องเป็นแกนของ production เมื่อโปรเจกต์นี้ย้ายไป production คาดว่าจะมี follow-up เทียบ Mapbox / MapTiler / self-hosted