DocsFlutter

Flutter Navigator - flutter_map + OSM

flutter_map renders OpenStreetMap tiles - no Google or Apple API key needed. We'll use it for the map, geolocator for GPS, and PickPoint for address search and routing. Everything runs on iOS and Android from a single codebase.

Never embed your PickPoint API key in client-side code. Browser JavaScript, iOS/Android apps, and Flutter bundles can all be inspected or decompiled. Always route geocoding and routing calls through your own backend. Exception: device tracking uses per-device client-id/client-secret credentials - safe to use directly from mobile/IoT apps. See the Backend Examples for production-ready proxy implementations.
Prerequisites: Flutter 3.x, a PickPoint API key. No map API key required. Add location permissions to both Info.plist (iOS) and AndroidManifest.xml (Android).
1

Add dependencies

# pubspec.yaml
dependencies:
  flutter_map: ^7.0.2      # OpenStreetMap tiles, no API key
  latlong2: ^0.9.1         # LatLng type used by flutter_map
  geolocator: ^13.0.1      # GPS location
  http: ^1.2.1             # HTTP requests
  socket_io_client: ^2.0.3 # optional - live tracking

For the user's location dot on the map, add flutter_map_location_marker: ^9.0.0 - it wraps geolocator and renders a pulsing blue dot automatically.

2

Set up the map

Use FlutterMap with the OpenStreetMap tile layer and a PolylineLayer that will render the route once fetched:

import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';

class MapScreen extends StatefulWidget {
  const MapScreen({super.key});
  @override State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  final _controller = MapController();
  List<LatLng> _routePoints = [];

  @override
  Widget build(BuildContext context) => FlutterMap(
    mapController: _controller,
    options: MapOptions(
      initialCenter: const LatLng(51.5, -0.12),
      initialZoom: 13,
    ),
    children: [
      TileLayer(
        urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
        userAgentPackageName: 'io.yourapp.navigator',
      ),
      if (_routePoints.isNotEmpty)
        PolylineLayer(polylines: [
          Polyline(points: _routePoints, color: const Color(0xFF008080), strokeWidth: 4),
        ]),
      const CurrentLocationLayer(),   // from flutter_map_location_marker
    ],
  );
}
3

Get the user's location

Request permission and get a position fix. For real-time navigation, subscribe to the position stream:

import 'package:geolocator/geolocator.dart';

Future<Position> getUserLocation() async {
  bool enabled = await Geolocator.isLocationServiceEnabled();
  if (!enabled) throw Exception('Location services disabled');

  var permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
  }

  return Geolocator.getCurrentPosition(
    locationSettings: const LocationSettings(accuracy: LocationAccuracy.high),
  );
}

// For continuous updates during navigation:
StreamSubscription<Position>? _positionStream;

void startTracking() {
  _positionStream = Geolocator.getPositionStream(
    locationSettings: const LocationSettings(accuracy: LocationAccuracy.high, distanceFilter: 10),
  ).listen((pos) {
    // update map, check re-routing, send to WebSocket
  });
}
4

Address search

Call /v2/address/search as the user types (debounce with a Timer). Pass the visible map bounds as viewbox:

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<List<Map<String, dynamic>>> searchAddress(
    String query, LatLngBounds bounds) async {
  final uri = Uri.https('api.pickpoint.io', '/v2/address/search', {
    'q':       query,
    'limit':   '6',
    'viewbox': '${bounds.west},${bounds.south},${bounds.east},${bounds.north}',
  });

  final resp = await http.get(uri, headers: {'X-Api-Key': apiKey});
  if (resp.statusCode != 200) throw Exception('Search failed');

  return (jsonDecode(resp.body) as List).cast<Map<String, dynamic>>();
  // Each item has "lat", "lon" (strings) and "display_name"
}

Show results in a ListView. On tap, use the result's lat/lon as the route destination.

5

Fetch and display the route

POST to /v2/route. Decode the encoded polyline from shape using a pub.dev package (e.g. google_polyline_algorithm), then call setState to add it to the map:

Future<List<LatLng>> fetchRoute(LatLng origin, LatLng dest) async {
  final resp = await http.post(
    Uri.parse('https://api.pickpoint.io/v2/route'),
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key':    apiKey,
    },
    body: jsonEncode({
      'locations': [
        {'lat': origin.latitude, 'lon': origin.longitude},
        {'lat': dest.latitude,   'lon': dest.longitude},
      ],
      'costing': 'auto',
    }),
  );

  final data  = jsonDecode(resp.body);
  final shape = data['trip']['legs'][0]['shape'] as String;
  return decodePolyline(shape);   // use the 'google_polyline' or 'polyline' pub.dev package
}
6

Optional: live tracking over WebSocket

Connect to PickPoint's tracking endpoint and emit location:add on each position update:

import 'package:socket_io_client/socket_io_client.dart' as IO;

final socket = IO.io('wss://ws.pickpoint.io', IO.OptionBuilder()
  .setTransports(['websocket'])
  .setPath('/v2/tracking')
  .setQuery({'client-id': deviceUid, 'client-secret': deviceSecret})
  .build());

socket.on('track:started', (data) {
  final trackUid = data['trackUid'] as String;
  // Now emit on each position update:
  _positionStream = Geolocator.getPositionStream().listen((pos) {
    socket.emit('location:add', {
      'trackUid':  trackUid,
      'latitude':  pos.latitude,
      'longitude': pos.longitude,
      'speed':     pos.speed,
      'heading':   pos.heading,
    });
  });
});

socket.emit('track:start', {});
socket.connect();

What's next

  • Voice guidance - use the flutter_tts package to speak each maneuver from trip.legs[0].maneuvers[].instruction.
  • Re-routing - on each position update, check the distance to the current route segment. If it exceeds 50 m, fetch a new route from the current position.
  • Custom map style - swap the OSM tile URL for any compatible tile server (e.g. Stadia Maps, MapTiler) that provides a style closer to your brand.
  • Vehicle profiles - expose a selector for costing (auto, bicycle, pedestrian) and re-fetch when it changes.

← Navigator AppiOS →Android →