Flutter Map: The Complete Developer Guide
Installation · Configuration · Real-World Code · Error Fixes
Based on a real parking-assistance app built with flutter_map + Riverpod
1. What is flutter_map?
flutter_map is a free, open-source map widget library for Flutter. Unlike Google Maps (which requires billing) or Mapbox (which needs a token), flutter_map renders tiles from any tile provider — most commonly OpenStreetMap — with zero cost and no API key required.
Key capabilities:
- Render OpenStreetMap, Mapbox, Google Maps (tile-compatible) and custom tile servers
- Add interactive Markers, Polylines, Polygons, and Circles
- Full MapController API for programmatic pan, zoom, and move
- Works on Android, iOS, Web, macOS, Linux and Windows
- Pairs naturally with latlong2 for coordinate maths
2. Installation
2.1 Add dependencies to pubspec.yaml
Open your pubspec.yaml file and add the following three packages under dependencies:
dependencies:
flutter:
sdk: flutter
# Map rendering
flutter_map: ^7.0.2
# Required companion — coordinate model
latlong2: ^0.9.0
# Optional: distance calculations (used in parking service)
# latlong2 re-exports Distance, so no extra package needed⚠️ Always check pub.dev for the latest version: https://pub.dev/packages/flutter_map
2.2 Run flutter pub get
flutter pub getYou should see output like:
Resolving dependencies…
+ flutter_map 7.0.2
+ latlong2 0.9.0
Changed 2 dependencies!2.3 Platform permissions
Android -android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>iOS — ios/Runner/Info.plist
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show nearby parking.</string>3. Basic Map Setup
The minimum viable FlutterMap requires three things: a MapOptions block, a TileLayer, and a size constraint. Add the imports first:
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';Then buid the widget:
FlutterMap(
options: MapOptions(
initialCenter: LatLng(6.9271, 79.8612), // Colombo
// can use google map to get latitude and longitude
initialZoom: 13.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
// 📌 {z}, {x}, {y} are placeholders that flutter_map replaces
// automatically with the correct tile coordinates.
userAgentPackageName: 'com.example.your_app',
),
],
)4. MapController — Programmatic Control
A MapController lets you move, zoom, or rotate the map from code — for example when the user taps a search result. Here is exactly how my parking app does it:
// 1. Declare the controller
final MapController _mapController = MapController();
// 2. Pass it to FlutterMap
FlutterMap(
mapController: _mapController,
options: MapOptions(initialCenter: userLocation, initialZoom: 13),
children: [ TileLayer(...) ],
)
// 3. Move programmatically — zoom to 15 when a parking lot is tapped
onTap: () {
setState(() { selectedParkingLot = lot; });
_mapController.move(lot.position, 15);
},
// 4. Jump back to user location
IconButton(
icon: const Icon(Icons.my_location),
onPressed: () => _mapController.move(userLocation, 15),
),5. Adding Markers
Markers are widgets pinned to latitude/longitude coordinates. You build a List<Marker> and pass it to MarkerLayer.
5.1 User Location Marker
Marker(
width: 30,
height: 40,
point: userLocation, // LatLng
alignment: Alignment.center,
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
),
child: const Icon(Icons.person_pin_circle,
color: Colors.white, size: 24),
),
)5.2 Dynamic Parking Lot Markers
The parking app builds markers in a _buildMarkers() method. Each parking lot marker is colour-coded: green = spaces available, red = full. It also shows the available slot count.
List<Marker> _buildMarkers() {
List<Marker> markers = [];
// ① Always add the user position first
markers.add(Marker(
point: userLocation,
width: 30, height: 40,
alignment: Alignment.center,
child: /* blue circle widget */
));
// ② Loop through filtered lots
for (final lot in _getFilteredParkingLots()) {
markers.add(Marker(
width: 50, height: 80,
point: lot.position, // LatLng stored in ParkingLot model
alignment: Alignment.center,
child: GestureDetector(
onTap: () {
setState(() => selectedParkingLot = lot);
_mapController.move(lot.position, 15);
},
child: Container(
decoration: BoxDecoration(
// Green if slots available, red if full
color: lot.hasAvailableSlots ? Colors.green : Colors.red,
shape: BoxShape.circle,
// Highlight selected marker with blue border
border: Border.all(
color: selectedParkingLot?.id == lot.id
? Colors.blue : Colors.white,
width: 3,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
lot.hasAvailableSlots
? Icons.local_parking
: Icons.block,
color: Colors.white, size: 20,
),
Text(lot.availableSlots.toString(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
)),
],
),
),
),
));
}
return markers;
}5.3 Wiring MarkerLayer into FlutterMap
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: userLocation,
initialZoom: 13.0,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.parking_app',
),
MarkerLayer(
markers: _buildMarkers(), // call your method here
),
],
)6. Distance Calculations with latlong2
The parking service uses the Distance class from latlong2 to filter and sort parking lots by their distance from the user. This works out-of-the-box — no GPS API calls needed.
import 'package:latlong2/latlong.dart';
final Distance distance = Distance();
// Get distance in kilometres between user and each parking lot
final distanceToLot = distance.as(
LengthUnit.Kilometer,
userLocation, // LatLng(6.9271, 79.8612)
lot.position, // LatLng stored in ParkingLot model
);
// Filter: keep only lots within 5 km
.where((lot) => lot.distanceFromUser <= radiusKm)
// Sort: closest first
nearbyLots.sort((a, b) =>
a.distanceFromUser.compareTo(b.distanceFromUser));7. Real-Time Map Updates
The parking app uses two complementary strategies: a periodic Timer that re-fetches every 2 minutes, and a Stream that pushes data on demand.
7.1 Periodic Timer
late Timer _refreshTimer;
@override
void initState() {
super.initState();
_loadParkingLots();
_setupPeriodicRefresh(); // start timer
_setupRealtimeUpdates(); // start stream
}
void _setupPeriodicRefresh() {
_refreshTimer = Timer.periodic(
const Duration(minutes: 2),
(timer) => _loadParkingLots(),
);
}
@override
void dispose() {
_refreshTimer.cancel(); // Always cancel timers!
_parkingStreamSubscription.cancel(); // Always cancel streams!
_searchController.dispose();
super.dispose();
}7.2 Stream Subscription
late StreamSubscription<List<ParkingLot>> _parkingStreamSubscription;
void _setupRealtimeUpdates() {
_parkingStreamSubscription =
_parkingService.getParkingLotsStream(userLocation).listen((lots) {
if (mounted) {
setState(() => parkingLots = lots);
}
});
}
// In ParkingService — emits every 30 seconds
Stream<List<ParkingLot>> getParkingLotsStream(LatLng userLocation) {
return Stream.periodic(
const Duration(seconds: 30),
(count) async => await getNearbyParkingLots(userLocation),
).asyncMap((future) => future);
}Always check mounted before calling setState() inside async callbacks to avoid “setState called after dispose” errors.
8. Full Widget build() — Map + Overlays
The complete Scaffold stacks the map underneath floating overlays (search bar, filter panel, bottom sheet, loading spinner, error banner) using a Stack widget.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('ParkSpot',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadParkingLots,
),
IconButton(
icon: const Icon(Icons.my_location),
onPressed: () => _mapController.move(userLocation, 15),
),
],
),
body: Stack(
children: [
// ① Base map layer
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: userLocation,
initialZoom: 13.0,
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.parking_app',
),
MarkerLayer(markers: _buildMarkers()),
],
),
// ② Loading overlay
if (isLoading)
Container(
color: Colors.black.withOpacity(0.3),
child: const Center(child: CircularProgressIndicator()),
),
// ③ Error banner
if (errorMessage != null)
Positioned(
top: 0, left: 0, right: 0,
child: /* error container widget */
),
// ④ Search bar at top
Positioned(
top: 0, left: 0, right: 0,
child: Column(
children: [
_buildSearchBar(),
if (showFilters) _buildFilterSheet(),
],
),
),
// ⑤ Bottom sheet (parking list)
_buildBottomSheet(),
],
),
);
}9. Search & Filter Integration
The parking app layers a search TextField over the map. Typing triggers _searchParkingLots() which re-fetches from the service and updates the markers automatically via setState().
final TextEditingController _searchController = TextEditingController();
Future<void> _searchParkingLots(String query) async {
if (query.isEmpty) { _loadParkingLots(); return; }
setState(() => isLoading = true);
try {
final lots = await _parkingService
.searchParkingLots(query, userLocation);
if (mounted) setState(() {
parkingLots = lots;
isLoading = false;
});
} catch (e) {
if (mounted) setState(() {
isLoading = false;
errorMessage = e.toString();
});
}
}
// Service-side search
Future<List<ParkingLot>> searchParkingLots(
String query, LatLng userLocation) async {
final filtered = _parkingLots.where((lot) {
return lot.name.toLowerCase().contains(query.toLowerCase())
|| lot.address.toLowerCase().contains(query.toLowerCase());
}).toList();
filtered.sort((a, b) =>
a.distanceFromUser.compareTo(b.distanceFromUser));
return filtered;
}10. Common Errors & Fixes
Below is a reference table of the most frequent flutter_map errors encountered in real projects, their causes, and step-by-step fixes.
10.1 Fixing “Blank Map / No Tiles”
This is the most common issue. Work through these steps in order:
Get Umesha Godawela’s stories in your inbox
Join Medium for free to get updates from this writer.
1. Check internet permission (Android AndroidManifest.xml)
2. Open https://tile.openstreetmap.org/13/6372/3925.png in a browser — if it loads, your URL template is fine
3. Ensure userAgentPackageName matches your app’s package name in pubspec.yaml
4. On emulator, go to Settings > Network and verify the emulator has internet
5. Add error/loading handlers to TileLayer to see tile-level errors in debug console
10.2 Fixing MapController Errors
// ❌ WRONG — called before FlutterMap is mounted
@override
void initState() {
super.initState();
_mapController.move(userLocation, 13); // crashes!
}
// ✅ CORRECT — use onMapReady callback
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: userLocation,
initialZoom: 13,
onMapReady: () {
// Safe to use _mapController here
},
),
children: [ TileLayer(...) ],
)10.3 Fixing setState After Dispose
// ❌ WRONG
final lots = await _parkingService.getNearbyParkingLots(userLocation);
setState(() { parkingLots = lots; }); // might be disposed by now
// ✅ CORRECT
final lots = await _parkingService.getNearbyParkingLots(userLocation);
if (mounted) {
setState(() { parkingLots = lots; isLoading = false; });
}11. Advanced Tips
11.1 Polylines — Draw a Route
PolylineLayer(
polylines: [
Polyline(
points: [
LatLng(6.9271, 79.8612), // start
LatLng(6.9147, 79.8572), // end (parking lot)
],
color: Colors.blue,
strokeWidth: 4.0,
),
],
)11.2 Circles — Show a Search Radius
CircleLayer(
circles: [
CircleMarker(
point: userLocation,
radius: 500, // 500 metres
useRadiusInMeter: true,
color: Colors.blue.withOpacity(0.1),
borderColor: Colors.blue,
borderStrokeWidth: 2,
),
],
)11.3 Using Riverpod with flutter_map
The parking app uses ConsumerStatefulWidget so it can read Riverpod providers alongside the map. The pattern is identical to a regular StatefulWidget — just extend ConsumerStatefulWidget and ConsumerState:
class ParkingMapScreen extends ConsumerStatefulWidget {
const ParkingMapScreen({super.key});
@override
ConsumerState<ParkingMapScreen> createState()
=> _ParkingMapScreenState();
}
class _ParkingMapScreenState
extends ConsumerState<ParkingMapScreen> {
// Access Riverpod providers with ref.watch / ref.read
// e.g. final settings = ref.watch(settingsProvider);
@override
Widget build(BuildContext context) {
return Scaffold( body: FlutterMap(...) );
}
}11.4 Performance: Cache Markers
If you have many markers, avoid rebuilding the list on every frame. Cache the result and only update when the underlying data changes:
List<Marker>? _cachedMarkers;
List<ParkingLot>? _lastLots;
List<Marker> _buildMarkers() {
final filtered = _getFilteredParkingLots();
if (filtered == _lastLots && _cachedMarkers != null) {
return _cachedMarkers!;
}
_lastLots = filtered;
_cachedMarkers = /* build list */;
return _cachedMarkers!;
}12. Quick Reference
// ── Installation ────────────────────────────────────────
// pubspec.yaml
// flutter_map: ^7.0.2
// latlong2: ^0.9.0
// ── Imports ─────────────────────────────────────────────
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
// ── Minimal map ──────────────────────────────────────────
FlutterMap(
options: MapOptions(
initialCenter: LatLng(lat, lng),
initialZoom: 13,
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.example.app',
),
MarkerLayer(markers: markers),
PolylineLayer(polylines: polylines),
CircleLayer(circles: circles),
],
)
// ── Controller ───────────────────────────────────────────
final ctrl = MapController();
ctrl.move(LatLng(lat, lng), zoom);
ctrl.rotate(degrees);
// ── Distance ─────────────────────────────────────────────
Distance().as(LengthUnit.Kilometer, pointA, pointB)Summary
flutter_map is one of the most versatile mapping solutions for Flutter. It requires zero paid API keys, renders OpenStreetMap tiles out of the box, and integrates cleanly with state management solutions like Riverpod. The key patterns from the parking app are:
• Use MapController for all programmatic moves — never manipulate state directly.
• Build markers dynamically from your data model and pass them to MarkerLayer.
• Always check mounted before calling setState() in async callbacks.
• Cancel Timers and StreamSubscriptions in dispose() to avoid memory leaks.
• Wrap the map in a Stack to layer search bars, bottom sheets, and loading spinners.
Happy mapping! 🗺️ For the latest flutter_map docs visit: https://docs.fleaflet.dev
Full code: https://github.com/Godawela/parking_assistence_app.git
To see the UI of the app visit my linkedin profile: www.linkedin.com/in/umesha-godawela22









