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.
client-id/client-secret credentials - safe to use directly from mobile/IoT apps. See the Backend Examples for production-ready proxy implementations.Info.plist (iOS) and AndroidManifest.xml (Android).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 trackingFor 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.
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
],
);
}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
});
}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.
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
}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_ttspackage to speak each maneuver fromtrip.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.