sharemap_maplib_flutter
Widget bản đồ dựa trên MapLibre dành cho Flutter, tích hợp với các API của ShareMap. Gói này đơn giản hóa việc tích hợp map tile, các lớp POI động như khu công nghiệp, cảng và nhà ga, đồng thời hỗ trợ xác thực request tài nguyên bằng API key và chữ ký HMAC.
Tính năng
- Tự động cấu hình lớp POI: Đăng ký source vector dùng chung và bật các layer shape/symbol đã được định nghĩa trong style ShareMap. Ứng dụng không cần tự phân tích style JSON.
- Chữ ký HMAC: Tự tính chữ ký HMAC Base64 động cho request tài nguyên qua
transformRequest. - Tối ưu so sánh sâu: So sánh danh sách
layerCodesđể tránh tải lại bản đồ hoặc lặp yêu cầu mạng không cần thiết. - Môi trường dev/prod: Chọn endpoint lớp POI dev/prod; chế độ dev đồng thời bật log chẩn đoán chi tiết.
- Hỗ trợ tile tùy chỉnh: Tích hợp MVT tileserver bên ngoài bằng URL trực tiếp.
- Vị trí nền native: Theo dõi vị trí bằng foreground service trên Android và Core Location trên iOS, chỉ bắt đầu sau thao tác rõ ràng của người dùng. Plugin phát dữ liệu qua Dart stream đã chuẩn hóa, không tự tải lên máy chủ và không lưu lịch sử vị trí.
Bắt đầu
Cài bản phát hành từ pub.flutter-io.cn:
dependencies:
sharemap_maplib_flutter: ^1.0.0
Hoặc pin Git tag của cùng bản phát hành:
dependencies:
sharemap_maplib_flutter:
git:
url: https://github.com/ShareMapLive/sharemap-maplib-flutter.git
ref: v1.0.0
Khi phát triển hai repository trên cùng máy, có thể dùng path local:
dependencies:
sharemap_maplib_flutter:
path: ../sharemap-maplib-flutter
Nâng cấp từ 0.0.5 lên 1.0.0
Bản 1.0.0 dùng transformRequest của MapLibre để xác thực từng request tài
nguyên. Loader tải style JSON trực tiếp và không chèn HMAC vào URL source,
tile hay glyph. Request tài nguyên dùng x-api-key header; khi bật
enableTilesToken và có token hợp lệ, request tới tile host dùng ?t=.
enableTilesToken mặc định vẫn tắt.
Sau khi cập nhật dependency, chạy flutter pub get, dừng app rồi build/run
lại hoàn toàn để cập nhật plugin native. Không thêm override trỏ về MapLibre
cũ không hỗ trợ flow này.
Yêu cầu tích hợp: Dart >=3.9.2, Flutter >=3.35.0; Android dùng JDK 21,
compileSdk 36, minSdk 24 và NDK 28. Dependency MapLibre đang ghim NDK
28.1.13356709; cài phiên bản này bằng Android SDK Manager và bảo đảm có
source.properties. iOS yêu cầu deployment target >=13.0.
Giới hạn iOS của bản 1.0.0: dependency được ghim ở
sharemap_maplibre_gl: 0.26.2+sharemap.1. Bản này còn dùng tên podspec
maplibre_gl thay vì sharemap_maplibre_gl, có thể chặn tích hợp CocoaPods.
Bản phát hành này chưa xác nhận build iOS thành công; cần bản dependency
sửa package identity trước khi coi iOS là được hỗ trợ đầy đủ.
Background Location vẫn là tính năng thử nghiệm, do nhóm phụ trách tiếp tục kiểm thử. Số phiên bản 1.0.0 không thay thế các giới hạn và checklist của tính năng này trong DEVICE_TEST_CHECKLIST.md.
Sử dụng bản đồ
Ví dụ khởi tạo widget ShareMapLibre:
import 'package:flutter/material.dart';
import 'package:sharemap_maplib_flutter/sharemap_maplib_flutter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ShareMap MapLibre Demo')),
body: const ShareMapLibre(
xApiKey: "YOUR_API_KEY",
secretKey: "YOUR_SECRET_KEY",
mode: ShareMapMode.prod,
mapStyle: ShareMapStyle.sharemap,
initialCameraPosition: CameraPosition(
target: LatLng(10.2312, 107.0134),
zoom: 11.0,
),
layerCodes: [
'vn-industrial-zones',
],
),
),
);
}
}
Tiles token — enableTilesToken
Tile được tính hạn mức theo project. Mặc định TẮT — bật lên là thêm một lời gọi mạng cho mọi app đang nâng phiên bản thư viện, nên phải là quyết định có ý thức.
ShareMapLibre(
xApiKey: 'YOUR_API_KEY',
secretKey: 'YOUR_SECRET_KEY',
enableTilesToken: true, // <- can dong nay, thieu la khong dem
initialCameraPosition: CameraPosition(target: LatLng(10.78, 106.70), zoom: 12),
)
| Tắt (mặc định) | Bật | |
|---|---|---|
| Tile được phục vụ | ✅ | ✅ |
| Rate limit | 300 req/phút mỗi IP | không áp |
| Đếm vào hạn mức project | ❌ | ✅ |
| Lời gọi mạng thêm | không | 1 lần/giờ |
Cách hoạt động khi bật
xApiKey + secretKey
│ POST /tiles/token (1 lan/gio, cache theo tien trinh)
▼
tiles token ──► dinh vao moi URL tile: ?t=<token>
Cần CẢ HAI key. Thiếu secretKey thì thư viện in cảnh báo một lần rồi chạy tầng miễn
phí, không ném lỗi.
Token đi qua query string, không qua header. Access log của CloudFront chỉ ghi Host,
Referer, User-Agent, Cookie — token gửi qua header sẽ được phục vụ nhưng không bao
giờ được đếm. Vì vậy khi đã có token, thư viện bỏ hẳn header x-api-key.
Chỉ tile của tiles.sharemap.live được đính token. POI layer (layers.sharemap.live)
và API Geomesh xác thực bằng HMAC x-api-key và không hiểu token — chúng giữ nguyên cách
xác thực cũ dù enableTilesToken bật hay tắt.
Token được lấy ngay khi map khởi tạo, không đợi tile đầu tiên — nếu đợi thì cả màn tile
đầu đã đi trước khi có token, tức không được đếm. transformRequest của MapLibre là đồng bộ
nên thư viện không chặn bản đồ để đợi token; tile nào đi trước khi token về thì rơi về tầng
miễn phí và vẫn được phục vụ.
Không ảnh hưởng lời gọi Geomesh
enableTilesToken chỉ đổi cách tile được xác thực. Các lời gọi API Geomesh
(GeomeshClient, LayerLoader.generateHmacCredential) không đổi — vẫn ký HMAC từng
request như trước.
Token tự làm mới thế nào
Theo nhu cầu, không có timer:
moi request tile
│
▼
con han? ──► con ──► dinh ?t= vao url
│
het
▼
KHONG gui token ──► request roi ve tang mien phi (200)
│
└─► lam moi NGAM, tile sau do co token
Điểm quan trọng: client tự kiểm hạn trước khi gửi. Token hết hạn thì không được gửi lên, nên không bao giờ có tile trắng — chỉ mất việc đếm cho vài tile đầu.
Token được cache theo tiến trình, khoá theo (tokenUrl, apiKey), nên vào lại màn hình
bản đồ không phải lấy token mới. Bản web cache ở sessionStorage; Flutter không có thứ
tương đương, và shared_preferences là async nên không thể đỡ cho một phép đọc đồng bộ.
Đăng xuất hoặc đổi tài khoản thì gọi clearTilesTokenCache().
Lưới an toàn: 403
Kiểm hạn phía client che được trường hợp thường, nhưng không che được lệch đồng hồ — client tưởng token còn hạn, edge tính là hết, gửi lên thì nhận 403 và tile trắng.
Bản web tự nối việc này qua map.on('error'). Plugin MapLibre của Flutter không phát sự
kiện lỗi tile, nên nếu app tự phát hiện được 403 thì báo về qua onTilesTokenSource:
ShareMapLibre(
enableTilesToken: true,
onTilesTokenSource: (source) => _tokenSource = source,
)
// ... khi app phat hien tile bi tu choi:
await _tokenSource?.notifyTileError(403);
Chỉ 403 được xử lý, và có nghỉ 60 giây giữa hai lần. Cooldown là bắt buộc — một khung
nhìn là vài chục tile, không có nó thì một lần hết hạn thành vài chục lời gọi
POST /tiles/token. Và nếu nguyên nhân là vượt hạn mức thì token mới cũng bị từ chối,
làm mới không giúp gì.
tilesTokenUrl
Đổi endpoint phát token cho môi trường dev/staging. Mặc định
https://geomesh.sharemap.live/tiles/token.
Thông tin thêm
Để tìm hiểu thêm về màu sắc giao diện, quản lý layer động và style tùy chỉnh, xem tài liệu trong repository hoặc liên hệ đội ngũ ShareMap Live tại sharemap.live.
Vị trí nền (bản thử nghiệm V1)
Vị trí nền là tính năng chủ động bật. Ứng dụng chỉ sử dụng bản đồ sẽ không tự nhận các khai báo quyền nhạy cảm. Sau khi thêm package, cấu hình host project một lần:
dart run sharemap_maplib_flutter:setup --dry-run
dart run sharemap_maplib_flutter:setup
dart run sharemap_maplib_flutter:doctor
Nếu đội chỉ tích hợp một nền tảng, chạy doctor đúng nền tảng đó:
dart run sharemap_maplib_flutter:doctor --platform=android
dart run sharemap_maplib_flutter:doctor --platform=ios
Android clean host đã được xác minh với compileSdk 36. Clean host iOS cũng đã
PASS cả Swift Package Manager và CocoaPods sau khi package ghim dependency
sharemap_maplibre_gl có đúng native package identity. Ứng dụng host vẫn phải
chạy doctor và clean build; không dùng thư mục Pods cũ làm bằng chứng tích
hợp. Xem quy trình trong hướng dẫn tích hợp.
Chỉ bắt đầu theo dõi từ một thao tác rõ ràng của người dùng khi ứng dụng đang hiển thị:
await ShareMapLibre.initialize(
androidNotification: const ShareMapAndroidNotificationOptions(
title: 'Đang chia sẻ vị trí',
content: 'Chạm để quay lại ứng dụng',
),
);
ShareMapLibre.locationStream.listen((location) {
// Ứng dụng host tự quyết định cách sử dụng điểm vị trí này.
});
final status = await ShareMapLibre.start(
options: const ShareMapTrackingOptions(
maxDuration: Duration(hours: 3),
android: ShareMapAndroidTrackingOptions(
interval: Duration(seconds: 60),
minUpdateInterval: Duration(seconds: 30),
minUpdateDistance: 50,
maxUpdateDelay: Duration(seconds: 120),
),
ios: ShareMapIOSTrackingOptions(
allowsBackgroundLocationUpdates: true,
desiredAccuracy: ShareMapIOSDesiredAccuracy.hundredMeters,
distanceFilter: 50,
activityType: ShareMapIOSActivityType.other,
pausesLocationUpdatesAutomatically: false,
),
),
);
maxDuration tính cả thời gian app hiển thị và ở nền. Đây là mốc tự dừng mục
tiêu; hệ điều hành có thể dừng phiên sớm hơn hoặc trì hoãn cleanup khi CPU/code
ứng dụng đang bị suspend. Bỏ maxDuration nghĩa là package không đặt deadline,
không phải cam kết phiên sống vĩnh viễn.
Gọi ShareMapLibre.stop() khi người dùng kết thúc phiên. Thư viện không có
profile; developer có thể dùng mặc định hoặc truyền trực tiếp bốn tham số
Android và năm tham số iOS. Developer nên bắt đầu từ
hướng dẫn tích hợp, cài đặt và API, sau đó đọc
hành vi theo nền tảng và
danh sách kiểm thử thiết bị.
Android không thể tự khởi động lại sau khi người dùng force-stop ứng dụng. Trên iOS, hệ điều hành quyết định thời điểm và tần suất giao vị trí; plugin không thể bảo đảm một khoảng thời gian cố định.