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 parameter GoogleMapController,

  • 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:

Left – unmarked restaurant, right – marked

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 Canvaswhich 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 initStateso 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:

Repository branch

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.

Similar Posts

Leave a Reply Cancel reply