Adding maps to a Flutter application is a powerful feature, but many developers think Google Maps is the only option. Fortunately, there are robust, open-source alternatives like the flutter_map
package, which uses OpenStreetMap data and offers incredible flexibility.
In this guide, we’ll build a simple application that displays a map with custom and interactive pins (markers). It’s the perfect foundation for any project that needs geolocation, from delivery apps to tourist guides.
Why use flutter_map
?
- It’s Open Source: Completely free and maintained by the community.
- Highly Customizable: Allows the use of various map providers and the creation of markers with any Flutter widget.
- Excellent Web Support: Works perfectly in web applications, as well as mobile and desktop.
Step 1: Creating the Project and Adding Dependencies
Let’s start by creating a new Flutter project and adding the necessary packages.
Create the project:
flutter create flutter_map_example
Navigate to the project folder:
cd flutter_map_example
Add the dependencies: We’ll need two packages.
flutter_map
for the map itself, andlatlong2
to make handling geographic coordinates easier.flutter pub add flutter_map latlong2
This command adds the necessary lines to your
pubspec.yaml
file and downloads the packages.
Step 2: The Map’s Structure
The structure of flutter_map
is based on layers. Think of it like stacking transparent sheets of paper: the first is the base map, the second contains the pins, the third could have polygons, and so on.
We will replace the content of the lib/main.dart
file with our code. The basic structure will be:
FlutterMap
: The main widget that holds the entire map.MapOptions
: Where we define the map’s initial center and zoom level.children
: A list of layers that will be drawn on the map.
Our first layer will be the TileLayer
, which is responsible for fetching and displaying the map “tiles” from OpenStreetMap.
Step 3: Creating Custom and Interactive Pins
The real magic of flutter_map
is in the MarkerLayer
. A Marker
(pin) can have any Flutter widget as its child
. This gives us complete freedom to create markers that are not just an icon, but also contain text, buttons, and gestures.
In our example, we’ll create a helper function _buildCityMarker
that returns a Marker
. This marker will be composed of:
- An
InkWell
to capture taps and provide visual feedback (like the mouse cursor changing on the web). - A
Column
to stack the icon and the text. - An
Icon
for the visual pin. - A
Container
with aText
to display the city’s name.
Step 4: The Complete Code
Now, let’s put it all together. Replace the entire content of your lib/main.dart
file with the code below. It’s complete, commented, and ready to run.
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ES Map',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MapaCidadesEsPage(),
debugShowCheckedModeBanner: false,
);
}
}
class MapaCidadesEsPage extends StatefulWidget {
const MapaCidadesEsPage({super.key});
@override
State<MapaCidadesEsPage> createState() => _MapaCidadesEsPageState();
}
class _MapaCidadesEsPageState extends State<MapaCidadesEsPage> {
// Geographic coordinates for the cities
static const LatLng _pontoSaoMateus = LatLng(-18.7160, -39.8582);
static const LatLng _pontoVitoria = LatLng(-20.3194, -40.3378);
// A central point to initialize the map between the two cities
static const LatLng _pontoCentral = LatLng(-19.5177, -40.0980);
// A list that will hold our pins (markers)
late final List<Marker> _markers;
@override
void initState() {
super.initState();
// Initialize the list of markers when the widget is created
_markers = [
// São Mateus Pin
_buildCityMarker(
point: _pontoSaoMateus,
cityName: 'São Mateus',
color: Colors.red,
),
// Vitória Pin
_buildCityMarker(
point: _pontoVitoria,
cityName: 'Vitória',
color: Colors.blue,
),
];
}
/// Helper function to create a custom Marker (pin)
Marker _buildCityMarker({
required LatLng point,
required String cityName,
required Color color,
}) {
return Marker(
point: point,
width: 95, // Fixed width that accommodates the longest name well
height: 65, // Fixed height for the pin
child: InkWell(
onTap: () {
// Action on pin click: shows a simple notification (SnackBar)
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('You clicked on $cityName!'),
backgroundColor: color,
duration: const Duration(seconds: 2),
),
);
},
// The visual content of the pin is a column with the icon and text
child: Column(
children: [
Icon(
Icons.location_pin,
color: color,
size: 40,
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withAlpha(204),
borderRadius: BorderRadius.circular(10),
),
child: Text(
cityName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Map - Espírito Santo'),
centerTitle: true,
),
// The main widget that renders the map
body: FlutterMap(
// Map options, such as initial center and zoom
options: const MapOptions(
initialCenter: _pontoCentral,
initialZoom: 8.0, // Zoom level that shows both cities
),
// The map is built in layers
children: [
// Layer 1: The base map from OpenStreetMap
TileLayer(
urlTemplate: '[https://tile.openstreetmap.org/](https://tile.openstreetmap.org/){z}/{x}/{y}.png',
userAgentPackageName: 'com.example.flutter_map_example',
),
// Layer 2: Our pins (markers)
MarkerLayer(
markers: _markers,
),
],
),
);
}
}
An Important Note about OpenStreetMap
When you run the app, you will notice an informational message in your debug console. It is not an error, but an important warning from the flutter_map
community: the public OpenStreetMap (OSM) servers are not intended for heavy commercial use.
OSM is an incredible project maintained by volunteers, and its servers have limited capacity. For development, testing, and personal projects, their use is acceptable. However, if you plan to release an app to a large audience, the recommended practice is to use a commercial tile provider, many of which offer generous free tiers. Switching providers usually just involves changing the urlTemplate
in your TileLayer
.
Conclusion
And there you have it! With just a few lines of code, we’ve created a functional and interactive map in Flutter. From here, the possibilities are enormous: you can load pins from an API, navigate to a detail screen when a marker is tapped, or even use other packages from the flutter_map
ecosystem to add pop-ups and marker clustering.
flutter_map
proves to be a powerful and accessible tool for any developer looking to integrate maps into their projects without relying on proprietary services.