Markers in Flutter Maps (SfMaps)

2 Nov 202124 minutes to read

Markers can be used to denote the locations. It is possible to use the built-in symbols or display a custom widget at a specific latitude and longitude on a map.

Adding markers

Shape layer

You can show markers at any position on the map by providing latitude and longitude position to the MapMarker, which is the widget returns from the markerBuilder property.

The markerBuilder callback will be called number of times equal to the value specified in the initialMarkersCount property. The default value of the initialMarkersCount property is null.

late List<Model> _data;
late MapShapeSource _dataSource;

@override
void initState() {
  _data = const <Model>[
    Model('Brazil', -14.235004, -51.92528),
    Model('Germany', 51.16569, 10.451526),
    Model('Australia', -25.274398, 133.775136),
    Model('India', 20.593684, 78.96288),
    Model('Russia', 61.52401, 105.318756)
  ];

  _dataSource = MapShapeSource.asset(
    'assets/world_map.json',
    shapeDataField: 'name',
    dataCount: _data.length,
    primaryValueMapper: (index) => _data[index].country,
  );
  super.initState();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
        child: Padding(
      padding: EdgeInsets.only(left: 15, right: 15),
      child: SfMaps(
        layers: <MapLayer>[
          MapShapeLayer(
            source: _dataSource,
            initialMarkersCount: 5,
            markerBuilder: (BuildContext context, int index) {
              return MapMarker(
                latitude: _data[index].latitude,
                longitude: _data[index].longitude,
                iconColor: Colors.blue,
              );
            },
          ),
        ],
      ),
    )),
  );
}

class Model {
  const Model(this.country, this.latitude, this.longitude);

  final String country;
  final double latitude;
  final double longitude;
}

default marker

NOTE

Tile layer

You can show markers at any position on the map by providing latitude and longitude position to the MapMarker, which is the widget returns from the MapTileLayer.markerBuilder property.

The markerBuilder callback will be called number of times equal to the value specified in the initialMarkersCount property. The default value of the initialMarkersCount property is null.

late List<Model> _data;

@override
void initState() {
  _data = const <Model>[
    Model('Brazil', -14.235004, -51.92528),
    Model('Germany', 51.16569, 10.451526),
    Model('Australia', -25.274398, 133.775136),
    Model('India', 20.593684, 78.96288),
    Model('Russia', 61.52401, 105.318756)
  ];

  super.initState();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: SfMaps(
        layers: <MapLayer>[
          MapTileLayer(
            urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            initialMarkersCount: 5,
            markerBuilder: (BuildContext context, int index) {
              return MapMarker(
                latitude: _data[index].latitude,
                longitude: _data[index].longitude,
                iconColor: Colors.blue,
              );
            },
          ),
        ],
      ),
    ),
  );
}

class Model {
  const Model(this.country, this.latitude, this.longitude);

  final String country;
  final double latitude;
  final double longitude;
}

Tile layer marker

NOTE

Appearance customization

You can customize the built-in markers appearance using the iconType, iconColor, iconStrokeColor, iconStrokeWidth, and size properties of the MapMarker.

  • Alignment - You can change the position of the marker from the given coordinate using the alignment property. The default value is Alignment.center. The available alignment options are topLeft, topRight, topCenter, centerLeft, center, centerRight, bottomLeft, bottomCenter, bottomRight.
  • Offset - You can adjust the marker position from the given coordinate using the offset property. The default value of the offset property is Offset.zero.

NOTE

  • The default value of the iconType is MapIconType.circle.
  • The default value of the iconStrokeWidth is 1.0.
  • The default value of the iconColor is Colors.blue.
  • The default value of the size is Size(14.0, 14.0).
late List<Model> _data;
late MapShapeSource _dataSource;

@override
void initState() {
  _data = <Model>[
    Model(-14.235004, -51.92528),
    Model(51.16569, 10.451526),
    Model(-25.274398, 133.775136),
    Model(20.593684, 78.96288),
    Model(61.52401, 105.318756)
  ];

  _dataSource = MapShapeSource.asset(
    'assets/world_map.json',
    shapeDataField: 'name',
  );
  super.initState();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
        child: Padding(
      padding: EdgeInsets.only(left: 15, right: 15),
      child: SfMaps(
        layers: <MapLayer>[
          MapShapeLayer(
            source: _dataSource,
            initialMarkersCount: 5,
            markerBuilder: (BuildContext context, int index) {
              return MapMarker(
                latitude: _data[index].latitude,
                longitude: _data[index].longitude,
                iconType: MapIconType.triangle,
                size: Size(18, 18),
                alignment: Alignment.center,
                offset: Offset(0, 9),
                iconColor: Colors.green[200],
                iconStrokeColor: Colors.green[900],
                iconStrokeWidth: 2,
              );
            },
          ),
        ],
      ),
    )),
  );
}

class Model {
  Model(this.latitude, this.longitude);

  final double latitude;
  final double longitude;
}

marker customization

Adding custom markers

You can show custom marker using the child property of the MapMarker which returns from the markerBuilder.

late List<Model> _data;
late List<Widget> _iconsList;
late MapShapeSource _dataSource;

@override
void initState() {
  _data = <Model>[
    Model(-14.235004, -51.92528),
    Model(51.16569, 10.451526),
    Model(-25.274398, 133.775136),
    Model(20.593684, 78.96288),
    Model(61.52401, 105.318756)
  ];

  _iconsList = <Widget>[
    Icon(Icons.add_location),
    Icon(Icons.airplanemode_active),
    Icon(Icons.add_alarm),
    Icon(Icons.accessibility_new),
    Icon(Icons.account_balance)
  ];

  _dataSource = MapShapeSource.asset(
    'assets/world_map.json',
    shapeDataField: 'name',
  );
  super.initState();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
        child: Padding(
      padding: EdgeInsets.only(left: 15, right: 15),
      child: SfMaps(
        layers: <MapLayer>[
          MapShapeLayer(
            source: _dataSource,
            initialMarkersCount: 5,
            markerBuilder: (BuildContext context, int index) {
              return MapMarker(
                latitude: _data[index].latitude,
                longitude: _data[index].longitude,
                child: _iconsList[index],
              );
            },
          ),
        ],
      ),
    )),
  );
}

class Model {
  Model(this.latitude, this.longitude);

  final double latitude;
  final double longitude;
}

custom marker

Adding markers dynamically

You can add markers dynamically using the insertMarker method. The markerBuilder will be called for the respective index once insertMarker method is called. The controller property of MapShapeLayer has to be set with the new instance of MapShapeLayerController.

Marker will be inserted at the given index if the index value is less than or equal to the current available index and the marker will be added as a last item if the index value is greater than the current available index.

NOTE

You can get the current markers count from MapShapeLayerController.markersCount.

For shape layer

late List<Model> _data;
late MapShapeLayerController _controller;
late MapShapeSource _dataSource;
late Random random;

@override
void initState() {
  _data = <Model>[
    Model(-14.235004, -51.92528),
    Model(51.16569, 10.451526),
    Model(-25.274398, 133.775136),
    Model(20.593684, 78.96288),
    Model(61.52401, 105.318756)
  ];

  _dataSource = MapShapeSource.asset(
    'assets/world_map.json',
    shapeDataField: 'name',
  );
  _controller = MapShapeLayerController();
  random = Random();
  super.initState();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
        child: Container(
      height: 350,
      child: Padding(
        padding: EdgeInsets.only(left: 15, right: 15),
        child: Column(
          children: [
            SfMaps(
              layers: <MapLayer>[
                MapShapeLayer(
                  source: _dataSource,
                  initialMarkersCount: 5,
                  markerBuilder: (BuildContext context, int index) {
                    return MapMarker(
                      latitude: _data[index].latitude,
                      longitude: _data[index].longitude,
                      child: Icon(Icons.add_location),
                    );
                  },
                  controller: _controller,
                ),
              ],
            ),
            ElevatedButton(
              child: Text('Add marker'),
              onPressed: () {
                _data.add(Model(-180 + random.nextInt(360).toDouble(),
                    -55 + random.nextInt(139).toDouble()));
                _controller.insertMarker(5);
              },
            ),
          ],
        ),
      ),
    )),
  );
}

class Model {
  Model(this.latitude, this.longitude);

  final double latitude;
  final double longitude;
}

For Tile layer

late List<Model> _data;
late MapTileLayerController _controller;
late Random random;

@override
void initState() {
  _data = <Model>[
    Model(-14.235004, -51.92528),
    Model(51.16569, 10.451526),
    Model(-25.274398, 133.775136),
    Model(20.593684, 78.96288),
    Model(61.52401, 105.318756)
  ];
  _controller = MapTileLayerController();
  random = Random();
  super.initState();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
        child: Container(
      height: 350,
      child: Padding(
        padding: EdgeInsets.only(left: 15, right: 15),
        child: Column(
          children: [
            SfMaps(
              layers: <MapLayer>[
                MapTileLayer(
                  urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
                  initialMarkersCount: 5,
                  markerBuilder: (BuildContext context, int index) {
                    return MapMarker(
                      latitude: _data[index].latitude,
                      longitude: _data[index].longitude,
                      child: Icon(Icons.add_location),
                    );
                  },
                  controller: _controller,
                ),
              ],
            ),
            ElevatedButton(
              child: Text('Add marker'),
              onPressed: () {
                _data.add(Model(-180 + random.nextInt(360).toDouble(),
                    -55 + random.nextInt(139).toDouble()));
                _controller.insertMarker(5);
              },
            ),
          ],
        ),
      ),
    )),
  );
}

class Model {
  Model(this.latitude, this.longitude);

  final double latitude;
  final double longitude;
}

Add markers dynamically

Updating the existing markers

You can update multiple markers at a same time by passing indices to the updateMarkers method in the MapShapeLayerController. The markerBuilder will be called again for the respective indices once updateMarkers method is called.

NOTE

late List<Model> _data;
late MapShapeLayerController _controller;
late Widget _markerWidget;
late MapShapeSource _dataSource;

@override
void initState() {
    _data = <Model>[
      Model(-14.235004, -51.92528),
      Model(51.16569, 10.451526),
      Model(-25.274398, 133.775136),
      Model(20.593684, 78.96288),
      Model(61.52401, 105.318756)
    ];

    _dataSource = MapShapeSource.asset(
       'assets/world_map.json',
        shapeDataField: 'name',
    );

    _controller = MapShapeLayerController();
    _markerWidget =  Icon(Icons.add_location);
    super.initState();
}

@override
Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Container(
            height: 350,
            child: Padding(
              padding: EdgeInsets.only(left: 15, right: 15),
              child: Column(
                children: [
                  SfMaps(
                    layers: <MapLayer>[
                      MapShapeLayer(
                        source: _dataSource,
                        initialMarkersCount: 5,
                        markerBuilder: (BuildContext context, int index){
                          return MapMarker(
                            latitude: _data[index].latitude,
                            longitude: _data[index].longitude,
                            child: _markerWidget,
                          );
                        },
                        controller: _controller,
                      ),
                    ],
                  ),
                  ElevatedButton(
                    child: Text('Update marker'),
                    onPressed: () {
                      List<int> updateList = <int>[1, 2];
                      _markerWidget = Icon(Icons.people);
                      _controller.updateMarkers(updateList);
                    },
                  ),
                ],
              ),
            ),
          )
      ),
   );
}

class Model {
  Model(this.latitude, this.longitude);

  final double latitude;
  final double longitude;
}

Update markers dynamically

Deleting a marker

You can remove marker at any index using the removeMarkerAt method.

NOTE

late List<Model> _data;
late MapShapeLayerController _controller;
late MapShapeSource _dataSource;

@override
void initState() {
    _data = <Model>[
      Model(-14.235004, -51.92528),
      Model(51.16569, 10.451526),
      Model(-25.274398, 133.775136),
      Model(20.593684, 78.96288),
      Model(61.52401, 105.318756)
    ];

    _dataSource = MapShapeSource.asset(
       'assets/world_map.json',
       shapeDataField: 'name',
    );
    _controller = MapShapeLayerController();
    super.initState();
}

@override
Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Container(
            height: 350,
            child: Padding(
              padding: EdgeInsets.only(left: 15, right: 15),
              child: Column(
                children: [
                  SfMaps(
                    layers: <MapLayer>[
                      MapShapeLayer(
                        source: _dataSource,
                        initialMarkersCount: 5,
                        markerBuilder: (BuildContext context, int index){
                          return MapMarker(
                            latitude: _data[index].latitude,
                            longitude: _data[index].longitude,
                            child: Icon(Icons.add_location),
                          );
                        },
                        controller: _controller,
                      ),
                    ],
                  ),
                  ElevatedButton(
                    child: Text('Remove marker'),
                    onPressed: () {
                      _controller.removeMarkerAt(4);
                    },
                  ),
                ],
              ),
            ),
          )
      ),
   );
}

class Model {
  Model(this.latitude, this.longitude);

  final double latitude;
  final double longitude;
}

Clearing the markers

You can clear all markers using the clearMarkers method.

NOTE

late List<Model> _data;
late MapShapeLayerController _controller;
late MapShapeSource _dataSource;

@override
void initState() {
    _data = <Model>[
      Model(-14.235004, -51.92528),
      Model(51.16569, 10.451526),
      Model(-25.274398, 133.775136),
      Model(20.593684, 78.96288),
      Model(61.52401, 105.318756)
    ];

    _dataSource = MapShapeSource.asset(
       'assets/world_map.json',
        shapeDataField: 'name',
    );
    _controller = MapShapeLayerController();
    super.initState();
}

@override
Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: Container(
            height: 350,
            child: Padding(
              padding: EdgeInsets.only(left: 15, right: 15),
              child: Column(
                children: [
                  SfMaps(
                    layers: <MapLayer>[
                      MapShapeLayer(
                        source: _dataSource,
                        initialMarkersCount: 5,
                        markerBuilder: (BuildContext context, int index){
                          return MapMarker(
                            latitude: _data[index].latitude,
                            longitude: _data[index].longitude,
                            child: Icon(Icons.add_location),
                          );
                        },
                        controller: _controller,
                      ),
                    ],
                  ),
                  ElevatedButton(
                    child: Text('Clear marker'),
                    onPressed: () {
                      _controller.clearMarkers();
                    },
                  ),
                ],
              ),
            ),
          )
      ),
   );
}

class Model {
  Model(this.latitude, this.longitude);

  final double latitude;
  final double longitude;
}

Marker controller

You can position the marker at the tapped position by converting touch pixel point into coordinates using the pixelToLatLng method of MapTileLayerController in the MapTileLayer and the MapShapeLayerController in the MapShapeLayer.

NOTE

It is applicable for both tile layer and shape layer.

Shape layer

late MapLatLng _markerPosition;
late _CustomZoomPanBehavior _mapZoomPanBehavior;
late MapShapeLayerController _controller;
late MapShapeSource _mapSource;

@override
void initState() {
   _controller = MapShapeLayerController();
   _mapZoomPanBehavior = _CustomZoomPanBehavior()
      ..zoomLevel = 1
      ..onTap = updateMarkerChange;
   _mapSource = MapShapeSource.asset(
      'assets/world_map.json',
      shapeDataField: 'continent',
   );
   super.initState();
}

void updateMarkerChange(Offset position) {
  _markerPosition = _controller.pixelToLatLng(position);

  /// Removed [MapShapeLayer.initialMarkersCount] property and updated
  /// markers only when the user taps.
  if (_controller.markersCount > 0) {
    _controller.clearMarkers();
   }
  _controller.insertMarker(0);
}

@override
Widget build(BuildContext context) {
  return Scaffold(
     body: Center(
        child: Container(
          height: 400,
          width: 400,
          child: MapShapeLayer(
            source: _mapSource,
            zoomPanBehavior: _mapZoomPanBehavior,
            controller: _controller,
            markerBuilder: (BuildContext context, int index) {
              return MapMarker(
                latitude: _markerPosition.latitude,
                longitude: _markerPosition.longitude,
                child: Icon(
                  Icons.location_on,
                  color: Colors.red,
                  size: 20,
                ),
              );
            },
          ),
        ),
      ),
   );
}

class _CustomZoomPanBehavior extends MapZoomPanBehavior {
  _CustomZoomPanBehavior();
  late MapTapCallback onTap;

  @override
  void handleEvent(PointerEvent event) {
    if (event is PointerUpEvent) {
      onTap(event.localPosition);
    }
    super.handleEvent(event);
  }
}

typedef MapTapCallback = void Function(Offset position);

Tile layer

late MapLatLng _markerPosition;
late _CustomZoomPanBehavior _mapZoomPanBehavior;
late MapTileLayerController _controller;

@override
void initState() {
   _controller = MapTileLayerController();
   _mapZoomPanBehavior = _CustomZoomPanBehavior()
      ..onTap = updateMarkerChange;
   super.initState();
}

void updateMarkerChange(Offset position) {
  _markerPosition = _controller.pixelToLatLng(position);

  /// Removed [MapTileLayer.initialMarkersCount] property and updated
  /// markers only when the user taps.
  if (_controller.markersCount > 0) {
    _controller.clearMarkers();
  }
  _controller.insertMarker(0);
}

@override
Widget build(BuildContext context) {
  return Scaffold(
     body: Center(
        child: Container(
          height: 400,
          width: 400,
          child: MapTileLayer(
            urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
            zoomPanBehavior: _mapZoomPanBehavior,
            controller: _controller,
            markerBuilder: (BuildContext context, int index) {
              return MapMarker(
                  latitude: _markerPosition.latitude,
                  longitude: _markerPosition.longitude,
                  child: Icon(
                    Icons.location_on,
                    color: Colors.red,
                    size: 20,
                  ));
            },
          ),
        ),
      ),
   );
}

class _CustomZoomPanBehavior extends MapZoomPanBehavior {
  _CustomZoomPanBehavior();
  late MapTapCallback onTap;

  @override
  void handleEvent(PointerEvent event) {
    if (event is PointerUpEvent) {
      onTap(event.localPosition);
    }
    super.handleEvent(event);
  }
}

typedef MapTapCallback = void Function(Offset position);

Position marker at tapped position

Zoom markers to fit bounds

You can visualize a specific area on the map by specifying the northeast and southwest coordinate points to the initialLatLngBounds property in the MapTileLayer and MapShapeLayer. It renders the map by calculating the center coordinate and zoom level depending on the initialLatLngBounds value.

The initialLatLngBounds property can be set at load time alone. You can use the latLngBounds property of MapZoomPanBehavior to dynamically update the map bounds.

late List<_TouristPlaceDetails> _touristPlaces;
late MapZoomPanBehavior _zoomPanBehavior;
bool _canFitMarkers = false;

@override
void initState() {
  _zoomPanBehavior = MapZoomPanBehavior();
  _touristPlaces = <_TouristPlaceDetails>[
    const _TouristPlaceDetails(
        MapLatLng(-25.6953, -54.4367), 'Iguazu Falls, Argentina'),
    const _TouristPlaceDetails(MapLatLng(-50.9423, -73.4068),
        'Torres del Paine National Park, Patagonia, Chile'),
    const _TouristPlaceDetails(
        MapLatLng(-15.9254, -69.3354), 'Lake Titicaca, Bolivia'),
    const _TouristPlaceDetails(
        MapLatLng(-13.1631, -72.5450), 'Machu Picchu, Peru'),
    const _TouristPlaceDetails(
        MapLatLng(-0.1862504, -78.5706247), 'The Amazon via Quito, Ecuador'),
    const _TouristPlaceDetails(
        MapLatLng(5.9701, -62.5362), 'Angel Falls, Venezuela'),
    const _TouristPlaceDetails(
        MapLatLng(-14.0875, -75.7626), 'Huacachina, Peru'),
    const _TouristPlaceDetails(
        MapLatLng(-22.7953, -67.8361), 'Laguna Verde, Bolivia'),
    const _TouristPlaceDetails(
        MapLatLng(-50.5025092, -73.1997346), 'Perito Moreno, Venezuela'),
    const _TouristPlaceDetails(
        MapLatLng(-22.9068, -43.1729), 'Rio de Janeiro, Brazil'),
    const _TouristPlaceDetails(
        MapLatLng(5.1765, -59.4808), 'Kaieteur Falls, Guyana'),
    const _TouristPlaceDetails(
        MapLatLng(-33.4489, -70.6693), 'Santiago, Chile'),
    const _TouristPlaceDetails(
        MapLatLng(4.7110, -74.0721), 'Bogota, Colombia'),
    const _TouristPlaceDetails(
        MapLatLng(-1.3928, -78.4269), 'Banos, Ecuador'),
  ];
  super.initState();
}

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        SfMaps(
          layers: <MapLayer>[
            MapTileLayer(
              urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
              zoomPanBehavior: _zoomPanBehavior,
              initialMarkersCount: _touristPlaces.length,
              markerBuilder: (BuildContext context, int index) {
                return MapMarker(
                  latitude: _touristPlaces[index].latLng.latitude,
                  longitude: _touristPlaces[index].latLng.longitude,
                  child: const Icon(
                    Icons.location_on,
                    color: Colors.red,
                    size: 20,
                  ),
                );
              },
            ),
          ],
        ),
        SizedBox(height: 10),
        Container(
          width: 300,
          padding: EdgeInsets.zero,
          child: CheckboxListTile(
            activeColor: Colors.blue,
            value: _canFitMarkers,
            title: Text('Zoom marker to fit bounds'),
            onChanged: (bool? value) {
              _canFitMarkers = value!;
              setState(() {
                if (_canFitMarkers) {
                  // South America bounds.
                  _zoomPanBehavior.latLngBounds = const MapLatLngBounds(
                      MapLatLng(12.434375, -34.80546874999999),
                      MapLatLng(-55.891699218750006, -91.654150390625));
                } else {
                  // World bounds.
                  _zoomPanBehavior.latLngBounds = const MapLatLngBounds(
                      MapLatLng(-90.0, -180.0), MapLatLng(90.0, 180.0));
                }
              });
            },
          ),
        ),
      ],
    ),
  );
}

class _TouristPlaceDetails {
  const _TouristPlaceDetails(this.latLng, this.place);
  final MapLatLng latLng;
  final String place;
}

Zoom markers to fit bounds

NOTE

You can refer to our Flutter Maps feature tour page for its groundbreaking feature representations. You can also explore our Flutter Maps Markers example that shows how to configure a Maps in Flutter.