Bỏ qua

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

  1. VietMap API key: register at https://maps.vietmap.vn/
  2. Add core packages:
    flutter pub add google_maps_flutter vietmap_flutter_plugin
    
  3. (Optional) Add http only if the app will call YOUR backend proxy:
    flutter pub add http
    
  4. Network access: HTTPS only (no special Android network security config).
  5. (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:

flutter pub add vietmap_flutter_plugin

Initialize once (e.g. main() or first screen initState):

Vietmap.getInstance('YOUR_API_KEY');

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}'),
);
Example: routing with error handling
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 duplicated fold
  • 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');
    }
);

Never hardcode sensitive API keys in the client. A minimal backend can:

  1. Accept restricted parameters from the mobile app (e.g., query, origin, destination).
  2. Append the server-held VietMap API key.
  3. Enforce rate limiting, auth (JWT/session), and input validation.
  4. 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

facebook
Tổng đài hỗ trợ
089.616.4567
facebook Chat Facebook zalo Chat Zalo