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ảngdanh 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.