Tích hợp bản đồ Việt Nam vào ứng dụng Flutter (Android, iOS) với maplibre_gl
Trả lời nhanh: dùng thư viện maplibre_gl
với styleString: 'https://map.vapp.vn/style.json'. Bản đồ hiện ngay, không cần khóa và không tính lượt. Chỉ khi ứng dụng gọi
API, ví dụ lấy tên địa điểm của điểm ghim, mới cần khóa API: tạo khóa miễn phí tại trang nhà phát triển.
Mọi lớp, tham số và hàm maplibre_gl trong mã Dart dưới đây đã được đối chiếu với mã nguồn bản
0.27.1 ngày 2026-10-19. Mã chưa được biên dịch thành ứng dụng thật: nếu gặp lỗi, hãy báo qua
support@erpfit.com để chúng tôi sửa.
Thử ngay: chạm để ghim, xem tên địa điểm
Bản đồ này dùng đúng style.json mà ứng dụng Flutter tải. Mỗi lần ghim, trang gọi
/api/v1/reverse giống hàm placeName() ở mục 5.
Điểm ghim: —
{}
Mỗi lần ghim là một lệnh gọi reverse tới map.vapp.vn, được ghi vào nhật ký truy vấn như mọi lệnh gọi API, xem Chính sách bảo mật.
1. Cái gì cần khóa, cái gì tính lượt
| Việc | Gọi tới | Cần khóa? | Tính vào hạn mức? |
|---|---|---|---|
| Hiển thị bản đồ | https://map.vapp.vn/style.json (tự tải tile và font) | Không | Không |
| Ghim địa điểm, vị trí hiện tại | xử lý trên máy | Không | Không |
| Tên địa điểm tại một tọa độ | GET /api/v1/reverse?lat=…&lng=… | Có (x-api-key) | 1 lượt mỗi lần gọi |
| Tìm địa điểm theo tên | GET /api/v1/autocomplete?q=… | Có (x-api-key) | 1 lượt mỗi lần gọi |
Bản đồ là vector tile, nên chỉ cần một đường dẫn style; không cần cấu hình nguồn tile riêng.
2. Cài đặt
flutter pub add maplibre_gl http
Android (android/app/src/main/AndroidManifest.xml, trong <manifest>):
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
iOS (ios/Runner/Info.plist):
<key>NSLocationWhenInUseUsageDescription</key>
<string>Ứng dụng dùng vị trí để hiện bạn đang ở đâu trên bản đồ.</string>
3. Lấy khóa API và giữ khóa ngoài mã nguồn
Đăng nhập trang nhà phát triển bằng Google, bấm Tạo khóa API và sao chép khóa (khóa chỉ hiện một lần). Không viết khóa thẳng vào mã và không đưa khóa lên GitHub; truyền khóa lúc chạy:
flutter run --dart-define=VAPP_MAP_KEY=vsk_xxxxxxxx...
const vappMapKey = String.fromEnvironment('VAPP_MAP_KEY');
Khóa nằm trong ứng dụng đã build, nên người khác vẫn có thể lấy được. Vì vậy khóa có giới hạn theo phút và theo ngày. Nếu khóa bị lộ, thu hồi nó trên trang nhà phát triển và tạo khóa mới.
4. Hiển thị bản đồ, chạm để ghim, vị trí hiện tại
import 'dart:math' show Point;
import 'package:flutter/material.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class BanDoKyNiem extends StatefulWidget {
const BanDoKyNiem({super.key});
@override
State<BanDoKyNiem> createState() => _BanDoKyNiemState();
}
class _BanDoKyNiemState extends State<BanDoKyNiem> {
MapLibreMapController? _map;
@override
Widget build(BuildContext context) {
return Stack(children: [
MapLibreMap(
styleString: 'https://map.vapp.vn/style.json',
initialCameraPosition: const CameraPosition(
target: LatLng(16.0, 106.0), // giữa Việt Nam
zoom: 4.8,
),
myLocationEnabled: true, // chấm xanh vị trí hiện tại
myLocationTrackingMode: MyLocationTrackingMode.none,
onMapCreated: (c) => _map = c,
onMapClick: _onTap, // chạm để ghim
),
// Ghi nguồn: điều kiện của gói miễn phí — vui lòng giữ.
const Positioned(
left: 8, bottom: 8,
child: DecoratedBox(
decoration: BoxDecoration(color: Color(0xCCFFFFFF)),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Text('© map.vapp.vn · © OpenStreetMap', style: TextStyle(fontSize: 11)),
),
),
),
]);
}
Future<void> _onTap(Point<double> _, LatLng at) async {
await _map?.addCircle(CircleOptions(
geometry: at,
circleRadius: 8,
circleColor: '#E11D48',
circleStrokeColor: '#FFFFFF',
circleStrokeWidth: 2,
));
final name = await placeName(at); // tùy chọn, tốn 1 lượt
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(name ?? 'Đã ghim')));
}
}
Để hiện các địa điểm đã lưu khi mở ứng dụng, đọc chúng từ bộ nhớ của bạn (ví dụ sqflite hoặc
shared_preferences) rồi gọi addCircle cho từng điểm, sau khi style đã tải xong: dùng tham số
onStyleLoadedCallback của MapLibreMap. Các điểm này không gọi API, nên không tính lượt.
5. Tên địa điểm của điểm ghim (reverse geocode)
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<String?> placeName(LatLng at) async {
final uri = Uri.https('map.vapp.vn', '/api/v1/reverse', {
'lat': at.latitude.toStringAsFixed(6),
'lng': at.longitude.toStringAsFixed(6),
});
final res = await http.get(uri, headers: {'x-api-key': vappMapKey});
if (res.statusCode != 200) return null; // 429 = vượt hạn mức, 401 = khóa hết hạn hoặc đã thu hồi
final r = jsonDecode(utf8.decode(res.bodyBytes))['result'];
return r?['name'] as String?;
}
result.name: tên địa điểm gần nhất.result.dist_m: khoảng cách từ điểm chạm tới địa điểm đó, tính bằng mét.result.place_id: mã ổn định (vm_…), có thể lưu cùng dữ liệu của bạn.- Mẹo: chỉ gọi khi người dùng lưu địa điểm, không gọi mỗi lần chạm. Như vậy bạn gần như không tốn lượt.
6. Mã lỗi
| Mã | Ý nghĩa | Nên làm |
|---|---|---|
| 401 | khóa hết hạn, đã thu hồi hoặc không đúng | tạo khóa mới trên trang nhà phát triển |
| 429 | vượt giới hạn theo phút, theo ngày hoặc theo tháng | đợi theo header retry-after; xem hạn mức đã dùng trên trang nhà phát triển |
7. Hỗ trợ
map.vapp.vn đang ở giai đoạn alpha: chúng tôi vận hành với nỗ lực tốt nhất và rất mong bạn góp ý. Viết tới support@erpfit.com. Các hướng dẫn khác: Hướng dẫn.