from simple to complex
I’m Tim, a developer at Goodworks. We recently made a restaurant guide app. We needed to display information about restaurants on the map, and the user could mark the ones he liked. I will show you how to work with maps in Flutter, as well as standard and non-standard markers. At the end of each part of the story there is a link to the repository with the full code of the example.
Map connection
As a cartographic basis, I chose Google Maps. To work with it in Flutter there is a package google_maps_flutter. The package is added as a dependency to the file pubspec.yaml:
dependencies:
...
google_maps_flutter: ^2.1.8
...
To connect to the maps, you will need an API key: how to get it is described in detail in documentation Maps SDK. For Android, add the key to the file android/app/src/main/AndroidManifest.xml:
<manifest ...
<application ...
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="API-КЛЮЧ"/>
After that, add a widget with a map to the file main.dart:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: CustomMap(),
),
);
}
}
class CustomMap extends StatefulWidget {
const CustomMap({Key? key}) : super(key: key);
@override
_CustomMapState createState() => _CustomMapState();
}
class _CustomMapState extends State<CustomMap> {
GoogleMapController? _controller;
static const LatLng _center = LatLng(48.864716, 2.349014);
void _onMapCreated(GoogleMapController controller) {
setState(() {
_controller = controller;
});
rootBundle.loadString('assets/map_style.json').then((mapStyle) {
_controller?.setMapStyle(mapStyle);
});
}
@override
Widget build(BuildContext context) {
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: const CameraPosition(
target: _center,
zoom: 12,
),
);
}
}
It is worth paying attention to:
-
method
_onMapCreated
: it is called when the map is created and receives as a parameterGoogleMapController
, -
parameter
initialCameraPosition
: defines the primary positioning of the map, -
GoogleMapController
: controls the map – positioning, animation, zoom.
To make the map more beautiful, I wrote the styles in the file assets/map_style.json. Styling a map is convenient with a service mapstyle.withgoogle.com. Now the map looks like this:

Repository branch: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/init-gm
Standard Markers
You can place standard markers on the map. This requires coordinates: in my case, they, like other restaurant data, are in the file datasource.dart
Method _upsertMarker
creates markers:
void _upsertMarker(Place place) {
setState(() {
_markers.add(Marker(
markerId: MarkerId(place.id),
position: place.location,
infoWindow: InfoWindow(
title: place.name,
snippet:
[...place.occasions, ...place.vibes, ...place.budget].join(", "),
),
icon: BitmapDescriptor.defaultMarker,
));
});
}
Class infoWindow
tapu shows a pin with information about the restaurant, and markers are added to the map using the attribute markers
widget GoogleMap
:
void _mapPlacesToMarkers() {
for (final place in _places) {
_upsertMarker(place);
}
}
...
@override
initState() {
super.initState();
_mapPlacesToMarkers();
}
@override
Widget build(BuildContext context) {
return GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: const CameraPosition(
target: _center,
zoom: 12,
),
markers: _markers,
);
}
It looks like this:
Repository branch: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/default-markers
Tapu cards
But a pin with information seemed not enough. I wanted to have a full-fledged card with a photo of the restaurant.
I will add a variable to store the selected location and methods to select it in _CustomMapState
. The card will be shown by tapping on the marker (method _selectPlace
), and disappear by tap where there is no marker (method _unselectPlace
). Cards are connected using a widget Positioned
:
class _CustomMapState extends State<CustomMap> {
...
final List<Place> _places = places;
Place? _selectedPlace;
void _unselectPlace() {
setState(() {
_selectedPlace = null;
});
}
void _selectPlace(Place place) {
setState(() {
_selectedPlace = place;
});
}
void _upsertMarker(Place place) {
setState(() {
_markers.add(Marker(
...
onTap: () => _selectPlace(place),
...
));
});
}
...
@override
Widget build(BuildContext context) {
return Stack(
...
children: <Widget>[
GoogleMap(
...
),
markers: _markers,
onTap: (_) => _unselectPlace(),
),
if (_selectedPlace != null)
Positioned(
bottom: 76,
child: PhysicalModel(
color: Colors.black,
shadowColor: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(12),
elevation: 12,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: MapPlaceCard(
place: _selectedPlace!,
),
),
),
),
],
);
}
Now the map – with cards:
Repository branch: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/cards-on-tap
Changing markers
It would be great if the user could mark the restaurants they liked, and the marker would change from this. To do this, you need icons:
Markers will be added using the method _upsertMarker
:
Future<void> _upsertMarker(Place place) async {
final selectedPrefix = place.id == _selectedPlace?.id ? "selected_" : "";
final favoritePostfix =
_likedPlaceIds.contains(place.id) ? "_favorite" : "";
final icon = await BitmapDescriptor.fromAssetImage(
const ImageConfiguration(),
"assets/icons/${selectedPrefix}map_place$favoritePostfix.png",
);
setState(() {
_markers.add(Marker(
markerId: MarkerId(place.id),
position: place.location,
onTap: () => _selectPlace(place),
icon: icon,
));
});
}
Heart-like is put by the method _likeTapHandler
:
void _likeTapHandler() async {
if (_selectedPlace == null) return;
setState(() {
if (_likedPlaceIds.contains(_selectedPlace!.id)) {
_likedPlaceIds.removeAt(_likedPlaceIds.indexOf(_selectedPlace!.id));
} else {
_likedPlaceIds.add(_selectedPlace!.id);
}
});
_upsertMarker(_selectedPlace!);
}
The method is called on the widget MapPlaceCard
:
@override
Widget build(BuildContext context) {
return Stack(
...
children: <Widget>[
...
if (_selectedPlace != null)
Positioned(
...
child: PhysicalModel(
...
child: Container(
...
child: MapPlaceCard(
place: _selectedPlace!,
isLiked: _likedPlaceIds.contains(_selectedPlace!.id),
likeTapHandler: _likeTapHandler,
),
),
),
),
],
);
}
When the user selects a different location, the icon should return to its previous state. This makes the method _unselectPlace
– it removes the selection from the place and updates its icon:
class _CustomMapState extends State<CustomMap> {
...
Future<void> _unselectPlace() async {
if (_selectedPlace == null) return;
final place = _selectedPlace;
setState(() {
_selectedPlace = null;
});
await _upsertMarker(place!);
}
Future<void> _selectPlace(Place place) async {
await _unselectPlace();
setState(() {
_selectedPlace = place;
});
await _upsertMarker(place);
}
...
@override
Widget build(BuildContext context) {
return Stack(
...
children: <Widget>[
GoogleMap(
...
),
markers: _markers,
onTap: (_) => _unselectPlace(),
),
if (_selectedPlace != null)
Positioned(
bottom: 76,
child: PhysicalModel(
color: Colors.black,
shadowColor: Colors.black.withOpacity(0.6),
borderRadius: BorderRadius.circular(12),
elevation: 12,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: MapPlaceCard(
place: _selectedPlace!,
isLiked: _likedPlaceIds.contains(_selectedPlace!.id),
likeTapHandler: _likeTapHandler,
),
),
),
),
],
);
}
}
Now our map looks like this:
Repository branch: https://github.com/gooditcollective/flutter-google-maps-exmaple/tree/different-icons
Non-standard markers
There is not much left – so that the name of the restaurant can always be seen, and not just by tapu. To do this, I had to make a separate utility for drawing a marker utils/custom_marker_drawer.dart
...
class CustomMarkerDrawer {
...
Future<CustomMarker> createCustomMarkerBitmap({
...
}) async {
...
PictureRecorder recorder = PictureRecorder();
Canvas canvas = Canvas(
recorder,
Rect.fromLTWH(
0,
0,
scaledCanvasWidth,
scaledCanvasHeight,
),
);
...
Picture picture = recorder.endRecording();
ByteData? pngBytes = await (await picture.toImage(
scaledCanvasWidth.toInt(),
scaledCanvasHeight.toInt(),
))
.toByteData(format: ImageByteFormat.png);
Uint8List data = Uint8List.view(pngBytes!.buffer);
final marker = BitmapDescriptor.fromBytes(data);
const double anchorDx = .5;
final anchorDy = imageHeight / scaledCanvasHeight;
return CustomMarker(marker: marker, anchorDx: anchorDx, anchorDy: anchorDy);
}
...
}
We draw on the virtual Canvas
which is then converted to Picture
by using PictureRecorder
. The result is converted to Uint8List
– a list of 8-bit unsigned integers, which we send to BitmapDescriptor
– an object that defines a bitmap, which Google Maps then draws on the map.
Flutter uses logical pixels to render. But, depending on the device, there can be several real pixels per logical pixel. To make the icons look correct regardless of the device, use the parameter scale
.
Here’s what it looks like in main.dart:
class _CustomMapState extends State<CustomMap> {
GoogleMapController? _controller;
final Set<Marker> _markers = {};
final CustomMarkerDrawer _markerDrawer = CustomMarkerDrawer();
double _scale = 1;
...
@override
initState() {
super.initState();
Future.delayed(Duration.zero, () {
_likedPlaceIds.addAll([_places[0].id, _places[3].id]);
_scale = MediaQuery.of(context).devicePixelRatio;
_mapPlacesToMarkers();
});
}
...
}
This parameter returns only the class MediaQueryData
– it can only be in the descendants of Material widgets, it is not in the root widget of the application. MediaQueryData.of(context)
will work only after full initialization initState
so I wrapped it in Future.delayed(Duration.zero, () {...}
– this transfers the execution of the code lying in it to the next processing tick, in which initState
already fully completed.
Final view of the map:
So, we have seen how to connect Google Maps in Flutter, how to use standard and non-standard markers. If you have any questions, I’ll be happy to answer.