Integrating VietMap APIs inside a Google Maps Flutter app¶
This guide shows how you can keep using google_maps_flutter while leveraging VietMap data via:
- Autocomplete / merged location search (API v4) for address & POI suggestions (Autocomplete v4)
- Geocode/Search results (API v4) parsed exactly per spec (Geocode v4)
- Place detail (API v4) to obtain coordinates from
ref_id(Place v4) - Route between two or more points and render a Polyline (Route v4)
Routing through the plugin
Vietmap.routing() in vietmap_flutter_plugin (0.5.2) still calls the legacy route endpoint (api-version=1.1). The response shape is the same as Route v4, so the polyline code below works with either. For toll costs (annotations=toll) or truck and container profiles, call Route v4 directly over HTTP.
- Decode the returned polyline (Google polyline level 5)
Note: If you need full VietMap map styling (vector tiles, traffic layers, advanced navigation), consider migrating to the full VietMap Map or Navigation SDK (Navigation: Flutter · Android · iOS). This document focuses on a hybrid approach where Google Maps is already embedded.
1. Prerequisites¶
- VietMap API key: register at https://maps.vietmap.vn/
- Add core packages:
- (Optional) Add
httponly if the app will call YOUR backend proxy: - Network access: HTTPS only (no special Android network security config).
- (Recommended) Lightweight backend proxy to protect the VietMap key (see section 9).
2. Example structure¶
You can either write raw HTTP code or (recommended) use the official vietmap_flutter_plugin to call VietMap APIs. Below we show the plugin approach (still rendering everything on a Google Map).
3. Autocomplete (Search) API v4¶
The new merged search uses v4 endpoint.
Basic endpoint:
GET https://maps.vietmap.vn/api/autocomplete/v4?apikey=YOUR_KEY&text=QUERY&focus=LAT,LON&display_type=1
Key parameters:
* text: user query (required)
* focus=lat,lng: optional bias
* display_type: choose response style (1 = new format; see official docs)
V4 Autocomplete & Search responses (per provided docs) are JSON arrays of objects (up to 10) containing fields such as ref_id, distance, address, name, display, boundaries[], categories[], entry_points[], and optionally data_old / data_new (variant objects with the same schema) depending on display_type (5 or 6). They DO NOT include latitude/longitude. To draw markers or routes you MUST perform a Place v4 lookup using the returned ref_id which yields lat & lng along with address components (hs_num, street, city, ward, ...).
Using plugin vs custom HTTP
| Aspect | Plugin (vietmap_flutter_plugin) |
Manual HTTP |
|---|---|---|
| Boilerplate | Minimal | Higher (models, parsing) |
| Future API changes | Abstracted | You must maintain |
| Error model | Unified Either<Failure,T> |
Exceptions / custom |
| Speed to integrate | Faster | Slower |
Install plugin:
Initialize once (e.g. main() or first screen initState):
Available calls (see plugin docs):
Vietmap.autocomplete(VietMapAutoCompleteParams(textSearch: 'Ho Chi Minh'));
Vietmap.geoCode(VietMapAutoCompleteParams(textSearch: '197 Tran Phu'));
Vietmap.place('ref_id');
Vietmap.routing(VietMapRoutingParams(points: [LatLng(10.76,106.7), LatLng(10.78,106.71)]));
Vietmap.reverse(LatLng(10.77,106.70));
Vietmap.matrix(VietmapMatrixParams(points: [...], sourcePoints: [...], destinationPoints: [...])) ;
Handle result (Either):
final res = await Vietmap.autocomplete(VietMapAutoCompleteParams(textSearch: query));
res.fold(
(failure) => debugPrint('Error: ${failure.message}'),
(list) => debugPrint('Suggestions: ${list.length}'),
);
final routingEither = await Vietmap.routing(
VietMapRoutingParams(points: [LatLng(originLat, originLng), LatLng(destLat, destLng)])
);
routingEither.fold(
(failure) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Route error: ${failure.message}'))),
(data) => debugPrint('Distance: ${data.paths.first.distance} m'),
);
4. Display on Google Map¶
Example widget (plugin-based): autocomplete search, pick 2 points (origin/destination) and draw a route.
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:vietmap_flutter_plugin/vietmap_flutter_plugin.dart' hide LatLng;
import 'package:vietmap_flutter_plugin/vietmap_flutter_plugin.dart'
as vietmap
show LatLng;
class VietMapGoogleIntegrationPage extends StatefulWidget {
const VietMapGoogleIntegrationPage({super.key});
@override
State<VietMapGoogleIntegrationPage> createState() =>
_VietMapGoogleIntegrationPageState();
}
class _VietMapGoogleIntegrationPageState
extends State<VietMapGoogleIntegrationPage> {
GoogleMapController? _mapController;
final Set<Marker> _markers = {};
final Set<Polyline> _polylines = {};
final TextEditingController _searchCtl = TextEditingController();
List<VietmapAutocompleteModel> _results = [];
VietmapAutocompleteModel? _fromAuto;
VietmapAutocompleteModel? _toAuto;
VietmapPlaceModel? _fromDetail;
VietmapPlaceModel? _toDetail;
bool _loading = false;
@override
void initState() {
super.initState();
Vietmap.getInstance('YOUR_VIETMAP_API_KEY');
}
Future<void> _search() async {
final q = _searchCtl.text.trim();
if (q.isEmpty) return;
setState(() => _loading = true);
final either = await Vietmap.autocomplete(
VietMapAutoCompleteParams(textSearch: q),
);
either.fold(
(f) => ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Search error: ${f.toString()}'))),
(list) => _results = list,
);
setState(() => _loading = false);
}
Future<void> _select(VietmapAutocompleteModel item) async {
setState(() => _loading = true);
Future<void> fetch(bool from) async {
final refId = item.refId;
if (refId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invalid place reference id')),
);
return;
}
final pe = await Vietmap.place(refId);
pe.fold(
(f) => ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Place error: ${f.toString()}'))),
(d) {
if (from) {
_fromDetail = d;
} else {
_toDetail = d;
}
_addMarker(
d,
from ? 'from' : 'to',
from ? BitmapDescriptor.hueAzure : BitmapDescriptor.hueRed,
);
},
);
}
if (_fromAuto == null) {
_fromAuto = item;
await fetch(true);
} else if (_toAuto == null) {
_toAuto = item;
await fetch(false);
await _buildRoute();
} else {
_reset();
_fromAuto = item;
await fetch(true);
}
setState(() => _loading = false);
}
void _addMarker(VietmapPlaceModel d, String id, double hue) {
if (d.lat == null || d.lng == null) return;
final m = Marker(
markerId: MarkerId(id),
position: LatLng(d.lat!.toDouble(), d.lng!.toDouble()),
infoWindow: InfoWindow(title: d.display),
icon: BitmapDescriptor.defaultMarkerWithHue(hue),
);
_markers.removeWhere((x) => x.markerId == m.markerId);
_markers.add(m);
}
Future<void> _buildRoute() async {
if (_fromDetail == null || _toDetail == null) return;
final either = await Vietmap.routing(
VietMapRoutingParams(
points: [
vietmap.LatLng(
_fromDetail!.lat!.toDouble(),
_fromDetail!.lng!.toDouble(),
),
vietmap.LatLng(
_toDetail!.lat!.toDouble(),
_toDetail!.lng!.toDouble(),
),
],
),
);
either.fold(
(f) => ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Route error: ${f.toString()}'))),
(r) {
if (r.paths?.isEmpty ?? true) return;
final path = r.paths!.first;
final points = path.points;
final pointsDecoded = points != null
? _decodePolyline6(points, 1e5)
: <LatLng>[];
final poly = Polyline(
polylineId: const PolylineId('route'),
color: Colors.blue,
width: 4,
points: pointsDecoded,
);
setState(
() => _polylines
..clear()
..add(poly),
);
_fit(pointsDecoded);
},
);
}
void _fit(List<LatLng> pts) {
if (_mapController == null || pts.isEmpty) return;
double minLat = pts.first.latitude,
maxLat = pts.first.latitude,
minLng = pts.first.longitude,
maxLng = pts.first.longitude;
for (final p in pts) {
if (p.latitude < minLat) minLat = p.latitude;
if (p.latitude > maxLat) maxLat = p.latitude;
if (p.longitude < minLng) minLng = p.longitude;
if (p.longitude > maxLng) maxLng = p.longitude;
}
_mapController!.animateCamera(
CameraUpdate.newLatLngBounds(
LatLngBounds(
southwest: LatLng(minLat, minLng),
northeast: LatLng(maxLat, maxLng),
),
48,
),
);
}
void _reset() {
setState(() {
_fromAuto = null;
_toAuto = null;
_fromDetail = null;
_toDetail = null;
_markers.clear();
_polylines.clear();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('VietMap Plugin + Google Maps'),
actions: [
IconButton(onPressed: _reset, icon: const Icon(Icons.refresh)),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _searchCtl,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Search (v4 via plugin)',
),
onSubmitted: (_) => _search(),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: _loading ? null : _search,
child: _loading
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Go'),
),
],
),
),
if (_results.isNotEmpty)
SizedBox(
height: 180,
child: ListView.separated(
itemCount: _results.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (_, i) {
final r = _results[i];
return ListTile(
dense: true,
title: Text(
r.display?.isNotEmpty == true
? r.display!
: (r.name ?? ''),
),
subtitle: Text(r.address ?? ''),
onTap: () => _select(r),
);
},
),
),
if (_fromDetail != null || _toDetail != null)
Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Row(
children: [
Expanded(
child: Text(
'From: ${_fromDetail?.display ?? '-'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
Expanded(
child: Text(
'To: ${_toDetail?.display ?? '-'}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
Expanded(
child: GoogleMap(
initialCameraPosition: CameraPosition(
target: LatLng(10.776889, 106.700806),
zoom: 12,
),
onMapCreated: (c) => _mapController = c,
markers: _markers,
polylines: _polylines,
myLocationEnabled: true,
compassEnabled: true,
),
),
],
),
);
}
static List<LatLng> _decodePolyline6(String encoded, double mul) {
// precision
var inv = 1.0 / mul;
var decoded = <LatLng>[];
var previous = [0, 0];
var i = 0;
// for each byte
while (i < encoded.length) {
// for each coord (lat, lon)
var latLng = [0, 0];
for (var j = 0; j < 2; j++) {
var shift = 0;
var byte = 0x20;
// keep decoding bytes until you have this coord
while (byte >= 0x20) {
byte = encoded.codeUnitAt(i++) - 63;
latLng[j] |= (byte & 0x1f) << shift;
shift += 5;
}
// add previous offset to get the final value and remember for the next one
latLng[j] =
previous[j] +
(latLng[j] & 1 == 1 ? ~(latLng[j] >> 1) : (latLng[j] >> 1));
previous[j] = latLng[j];
}
// scale by precision and chop off long coords also flip the positions so
// it's the far more standard lon,lat instead of lat,lon
var temp = LatLng(latLng[0] * inv, latLng[1] * inv);
decoded.add(temp);
}
// hand back the list of coordinates
return decoded;
}
}
5. Notes & best practices¶
- Apply debounce (e.g. 300 ms) + caching to reduce Autocomplete quota usage
- For biasing Autocomplete, pass
focus=lat,lng(approx user location) - Always fetch coordinates via Place v4 (
Vietmap.place(refId)) because Autocomplete/Search v4 results omit coordinates - Centralize handling of
Either<Failure,T>to reduce duplicatedfold - Debounce text input (e.g. 300–400 ms) before calling
Vietmap.autocomplete - Handle errors gracefully; surface limited user-facing messages
6. Extensions¶
| Requirement | Approach |
|---|---|
| Turn-by-turn navigation | VietMap Navigation SDK (Flutter) |
| Toll / fee estimation | Route v4 with annotations=toll (Route v4) |
| Multi-stop optimization | TSP v4 for one vehicle, VRP for a fleet, or Matrix v4 + your own solver |
| Truck constraints | Include vehicle=truck + required fields (see Route v4) |
7. Basic error handling¶
Using the plugin you normally handle Either<Failure,T>:
final result = await Vietmap.routing(
VietMapRoutingParams(points:[LatLng(oLat,oLng),LatLng(dLat,dLng)])
);
result.fold(
(failure){
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Route error: ${failure.message}'))
);
},
(data){
if(data.paths.isEmpty){ debugPrint('No path'); return; }
debugPrint('Distance: ${data.paths.first.distance} m');
}
);
8. Secure backend proxy (recommended)¶
Never hardcode sensitive API keys in the client. A minimal backend can:
- Accept restricted parameters from the mobile app (e.g.,
query,origin,destination). - Append the server-held VietMap API key.
- Enforce rate limiting, auth (JWT/session), and input validation.
- Optionally cache popular responses (e.g., Autocomplete for identical query+focus for 30–60s).
9. Conclusion¶
You can quickly enrich an existing Google Maps Flutter app using VietMap Autocomplete v4 and Route v4 without a full migration. As needs grow (vector styling, offline, navigation), consider the full Navigation SDK: Flutter · Android · iOS.
© VietMap