Mastering Flutter Map: A Practical Guide?-?Part?1

Mastering Flutter Map: A Practical Guide?-?Part?1

Hello everyone,

This is not just an introduction to this article but also to a new series of articles on how to build a production app that requires maps. Let’s keep things concise, useful, and clear by asking questions and answering them directly. This will make the process easier to follow.

What is Flutter?Map?

The answer is straightforward, and I’ll quote directly from the MapStadia documentation:

“Flutter Map is a Flutter-first map library built from the ground up (not a wrapper for an existing native library) and boasts broad cross-platform compatibility due to its design. It is loosely inspired by Leaflet, a JavaScript raster map library, and features a wide range of plugins.”

Additionally, the Flutter Map documentation states:

“A versatile mapping package for Flutter. Simple and easy to learn, yet completely customizable and configurable, it’s the best choice for mapping in your Flutter app.”

Sources:

How Does It?Work?

Let’s dive right in and learn as we go.

Showing Our Flutter?Map

To achieve this, follow these steps:

  1. Create a Flutter project as you normally would.
  2. Install the flutter_map & latlong2 packages.
  3. Create a file and name it map_page (optional).
  4. Copy and paste the following code:

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';

class MapPage extends StatefulWidget {
  const MapPage({super.key});

  @override
  State<MapPage> createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
  final MapController mapController = MapController();

  @override
  Widget build(BuildContext context) {
    return FlutterMap(
      mapController: mapController,
      options: MapOptions(
        initialCenter: const LatLng(33.583760, -7.648748),
        initialZoom: 15,
      ),
      children: [
        TileLayer(
          urlTemplate:
              'https://{s}.google.com/vt/lyrs=m,h&x={x}&y={y}&z={z}&hl=ar-MA&gl=MA',
          subdomains: const ['mt0', 'mt1', 'mt2', 'mt3'],
          userAgentPackageName: 'com.example.app',
        ),
        const RichAttributionWidget(
          attributions: [
            TextSourceAttribution(
              'OpenStreetMap contributors',
              // onTap: () =>
              // launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
            ),
          ],
        ),
      ],
    );
  }
}        

Explaining the?Code

  • FlutterMap: The main widget that helps create the map. Everything related to the map exists inside this widget.
  • mapController: As the name suggests, it helps you control the map’s camera and provides several helper functionalities.
  • initialCenter: This parameter positions the camera at a specific point when the map is first loaded.
  • urlTemplate: Creates a URL/URI to fetch specific map tiles.

Note: I encourage everyone, especially Moroccan developers, to use URLs that accurately represent our territory. I’ve noticed that some applications using OpenStreetMap have incorrect tile URLs.

  • subdomains: Some tile servers use multiple subdomains (e.g., 'a', 'b', 'c') to distribute requests. While this was useful for older browsers with connection limitations, modern protocols like HTTP/2 and HTTP/3 make subdomains less necessary. Avoid using them if your tile server supports modern protocols, as they can hinder performance.
  • userAgentPackageName: This parameter should include your app’s package name (e.g., 'com.example.app'). It helps avoid being blocked by tile servers due to unidentified traffic. If no value is provided, it defaults to 'unknown'. (Source: Flutter Map Docs)

Moving Forward: MarkerLayer and?Marker

Imagine you have a restaurant and want to highlight it on the map like this:

Markers are perfect for this. Here’s how you can create one:

const MarkerLayer(
    markers: [
      Marker(
        point: LatLng(33.583760, -7.648748),
        width: 80,
        height: 80,
        child: CircleAvatar(
          backgroundColor: Colors.white,
          child: CircleAvatar(
            radius: 35.0,
            backgroundImage: AssetImage("assets/images/restaurent.jpg"),
          ),
        ),
      ),
    ],
  ),        

Adding a Pop-Up to the?Marker

What if you want to show a pop-up when the marker is clicked, like this?


While there’s no direct parameter in the marker widget for this, Flutter Map has a rich ecosystem of plugins. For this functionality, we’ll use the flutter_map_marker_popup package.

  1. Go to Pub.dev and get the flutter_map_marker_popup package.
  2. Download three images from the internet.
  3. Write the following code:

final List<String> restaurentPresentations = [
    "? We guarantee that you'll experience the traditional Moroccan vibe, from the beautiful lighting to the amazing cuisine. The environment also offers plenty to enhance your overall experience.",
    "?? Imagine enjoying your favorite meal in a serene place facing the sea, with the sky stretching endlessly, free from any barriers or limitations. You're welcome!",
    "?? Are you looking for a calm place with a green environment and a great chef who can cook the best meal for you? If so, we are waiting for you!",
  ];

// List of markers
static final List<LatLng> markerPositions = [
  const LatLng(33.592638, -7.643341),
  const LatLng(33.594438, -7.655744),
  const LatLng(33.583760, -7.648748),
];

// List of images
static final List<String> images = [
  "assets/images/restaurent.jpg",
  "assets/images/restaurent_2.jpg",
  "assets/images/restaurent_3.jpg",
];

// Mapping marker positions to Marker widgets with corresponding images
static final List<Marker> markers = 
  List.generate(markerPositions.length, (index) {
    return Marker(
      point: markerPositions[index],
      width: 80,
      height: 80,
      child: CircleAvatar(
        backgroundColor: Colors.white,
        child: CircleAvatar(
          radius: 35.0,
          backgroundImage: AssetImage(
            images[index],
          ),
        ),
      ),
    );
});        

Explaining the?Code

Here, I created a list of image URLs and a list of markers. Then, I mapped the markers and assigned each one an image. Next, I used the PopupMarkerLayer inside the children parameter:

PopupMarkerLayer(
  options: PopupMarkerLayerOptions(
    markers: markers,
    popupDisplayOptions: PopupDisplayOptions(
      builder: (BuildContext context, Marker marker) => SizedBox(
        width: 300,
        child: Card(
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              restaurentPresentations[markers.indexOf(marker)],
            ),
          ),
        ),
      ),
    ),
  ),
),        

  • PopupMarkerLayerOptions: Contains two key parameters:
  • markers: Takes the list of markers we built earlier.
  • builder: Returns a widget that represents the pop-up. Notice how I used dynamic text (restaurentPresentations[markers.indexOf(marker)]) to simulate data coming from an API.

Showing a Bottom Sheet on Marker?Tap

Another common functionality is showing a bottom sheet when a marker is tapped, like this:


This is similar to showing a pop-up but offers more options. You can achieve this by modifying the marker widget to display a bottom sheet (a built-in Flutter widget) or by using a package like sliding_up_panel.

To implement this using the first approach, update your markers code and call it in the MarkerLayer:

final List<Marker> markers = List.generate(markerPositions.length, (index) {
  return Marker(
    point: markerPositions[index],
    width: 80,
    height: 80,
    child: GestureDetector(
      onTap: () => showModalBottomSheet(
        context: context,
        builder: (BuildContext context) {
          return Padding(
            padding: const EdgeInsets.all(16.0),
            child: Text(restaurentPresentations[index]),
          );
        },
      ),
      child: CircleAvatar(
        backgroundColor: Colors.white,
        child: CircleAvatar(
          radius: 35.0,
          backgroundImage: AssetImage(images[index]),
        ),
      ),
    ),
  );
});        

Note: I used GestureDetector instead of InkWell because InkWell requires a Material widget, which could complicate things. GestureDetector works perfectly for this use case. I also used showModalBottomSheet to display a modal bottom sheet with more information about the restaurant.

In your children, add the MarkerLayer and pass the markers like this:

MarkerLayer(
  markers: markers,
  ),        

Polygons: Building a Construction Map

Have you ever seen a construction site with a plot of land divided into sections, each with a specific color and labeled for future construction (e.g., school, gym, supermarket)? Each section is a polygon. Let’s build one and then create a construction app to show future projects.

To build a polygon, use the PolygonLayer and pass a list of polygons to it:

PolygonLayer(
    polygons: [
      Polygon(
        points: [
          const LatLng(33.595593, -7.655844),
          const LatLng(33.595081, -7.641224),
          const LatLng(33.590756, -7.641067),
          const LatLng(33.591054, -7.655844),
        ],
        color: Colors.blue.withOpacity(0.7),
        label: "Big school",
        labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
      ),
    ],
  ),        

  • points: Defines the vertices of the polygon.

You can add multiple polygons to the PolygonLayer to show different sections of the construction site:

PolygonLayer(
  polygons: [
    Polygon(
      points: [
        const LatLng(33.595593, -7.655844),
        const LatLng(33.595081, -7.641224),
        const LatLng(33.590756, -7.641067),
        const LatLng(33.591054, -7.655844),
      ],
      color: Colors.blue.withOpacity(0.7),
      label: "University",
      labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24),
    ),
    Polygon(
      points: [
        const LatLng(33.590922, -7.646117),
        const LatLng(33.590756, -7.641067),
        const LatLng(33.586216, -7.641539),
        const LatLng(33.587026, -7.646474),
      ],
      color: Colors.green.withOpacity(0.7),
      label: "Mall",
      labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
    ),
    Polygon(
      points: [
        const LatLng(33.591054, -7.655844),
        const LatLng(33.59097, -7.650136),
        const LatLng(33.587074, -7.649822),
        const LatLng(33.587408, -7.655701),
      ],
      color: Colors.red.withOpacity(0.7),
      label: "Sports Club",
      labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
    ),
  ],
)        

Note: If the label doesn’t appear, adjust the text size to fit within the polygon.

Adding Interactivity to?Polygons

Let’s take it a step further. Imagine a real estate company wants to display future construction plans on the map, and when someone taps on a specific area (a polygon), they can get more information. How would you achieve this?

Here’s the solution:

  1. Detect the point where the user taps on the map.
  2. Check if the point lies within the polygon.
  3. If it does, display additional information.

Here’s where the fun begins: How do you detect if a point lies inside a polygon?

This problem is known as Point in Polygon (PIP), and one solution is the Ray Casting Algorithm. This algorithm is widely used in fields like computer vision, geographic information systems (GIS), and computer-aided design (CAD).

To learn more about the Ray Casting Algorithm, check out these resources:

Applying the Ray Casting Algorithm in?Flutter

Fortunately, the Flutter community has already solved this problem with the point_in_polygon package by using Ray Casting Algorithm. Here’s how to use it:

  • Create a list of LatLng points before the build method:

// list of polygon points
final List<LatLng> polygonPoints = [
  const LatLng(33.595593, -7.655844),
  const LatLng(33.595081, -7.641224),
  const LatLng(33.590756, -7.641067),
  const LatLng(33.591054, -7.655844),
];

@override
Widget build(BuildContext context) {
  // ... rest of your build method
}        

  • Pass this list to the points parameter of your polygon:

PolygonLayer(
  polygons: [
    Polygon(
      points: polygonPoints,
      color: Colors.blue.withOpacity(0.7),
      label: "University",
      labelStyle: const TextStyle(
        fontWeight: FontWeight.bold,
        fontSize: 24,
      ),
    ),
  ],
)        

  • Add the point_in_polygon package to your pubspec.yaml file.
  • Apply the algorithm → onTap:

@override
Widget build(BuildContext context) {
  return FlutterMap(
    mapController: mapController,
    options: MapOptions(
      initialCenter: const LatLng(33.583760, -7.648748),
      initialZoom: 15,
      onTap: ((tapPosition, point) {
        debugPrint("This is the point with coordinates (x, y): $point");
        Point tapedPoint = Point(y: point.latitude, x: point.longitude);

        final List<Point> polygonPoints = [
          Point(y: 33.595593, x: -7.655844),
          Point(y: 33.595081, x: -7.641224),
          Point(y: 33.590756, x: -7.641067),
          Point(y: 33.591054, x: -7.655844),
        ];

        if (Poly.isPointInPolygon(tapedPoint, polygonPoints)) {
          showModalBottomSheet(
            context: context,
            builder: (BuildContext context) {
              return const Padding(
                padding: EdgeInsets.all(16.6),
                child: Text(
                  "This is a large university offering over 30 specialties in web and mobile development, AI, and cybersecurity. It is a public university with various research laboratories, spaces dedicated to applied technology, and areas for robotics competitions. There are also workshops equipped with all types of 3D printers, CNC machines, and automated control systems. We also have a data center and a supercomputer for AI research. You are welcome to join us!",
                ),
              );
            },
          );
        } else {
          debugPrint("This point: $point is not inside the polygon.");
        }
      }),
    ),
    children: [
      // ... rest of your children
    ],
  );
}        

Explaining the?Code

  • onTap: Detects the point where the user taps on the map.
  • tapedPoint: Converts the tapped point from LatLng to Point (required by the package).

You should know that this package doesn’t use LatLng for points but instead uses a class called Point. For example, instead of writing const LatLng(33.595593, -7.655844), you will write Point(y: 33.595593, x: -7.655844).

  • Poly.isPointInPolygon: Checks if the tapped point lies within the polygon.
  • showModalBottomSheet: Displays additional information if the point is inside the polygon.

But if the point is not inside the polygon, we’ll get this message with the point:

Polylines: Building a TGV Route?Map

Imagine you’re working on an app for a train company building a TGV in Morocco. You need to show the progress of the construction by drawing polylines between cities (e.g., Tangier to Casablanca).

To draw a polyline, use the PolylineLayer:

First, to see the polyline that will pass through several cities from Tangier to Casablanca, minimize the initial zoom to 8 instead of the previous 15, like this:

@override
Widget build(BuildContext context) {
  return FlutterMap(
    mapController: mapController,
    options: MapOptions(
      initialCenter: const LatLng(33.583760, -7.648748),
      initialZoom: 8,
    ),
    // ... rest of your FlutterMap properties
  );
}        
// Draw a polyline

PolylineLayer(
  polylines: [
    Polyline(
      points: const [
        LatLng(35.7712654, -5.7865388),
        LatLng(35.0044751, -5.9133911),
        LatLng(34.2513818, -6.5983018),
        LatLng(34.0164539, -6.8382672),
        LatLng(33.9173753, -6.9307315),
        LatLng(33.5897945, -7.5940499),
      ],
      color: Colors.blue,
      strokeWidth: 2.0,
      borderStrokeWidth: 4.0,
      borderColor: Colors.yellow,
    ),
  ],
)        

Making Polylines Interactive

To make the polyline tappable, you can use the flutter_map_tappable_polyline package or create a custom solution based on your needs.

Circle Layer: Visualizing Farms

In Saudi Arabia, farms are often circular due to the center pivot irrigation system. If your company wants to build an app to help these farmers, you can represent each farm as a circle on the map.

Use the CircleLayer to achieve this:

CircleLayer(
  circles: [
    CircleMarker(
      point: const LatLng(33.590756, -7.641067),
      radius: 1000,
      useRadiusInMeter: true,
      color: Colors.green.withOpacity(0.5),
      borderStrokeWidth: 4.0,
    ),
  ],
)        

Attribution Layer

The AttributionLayer helps you credit the sources you use, as required by their terms of service. Use the RichAttributionWidget class for this:

RichAttributionWidget(
    attributions: [
      TextSourceAttribution(
        'OpenStreetMap contributors',
        onTap: () =>
            launchUrl(Uri.parse('https://openstreetmap.org/copyright')),
      ),
    ],
  ),        

Note: Don’t forget to use the url_launcher package to handle URLs properly.

In the next part of this series, we’ll explore how to draw routes that users can follow from point A to point B. Stay tuned!

I hope you enjoyed reading and applying this article. Feel free to share your opinions or experiences, and if you have any questions, you're welcome to ask. Thanks for reading, and I hope you apply its content. Stay tuned for the upcoming parts of our Flutter map series.


要查看或添加评论,请登录

Imad Eddarraz的更多文章

社区洞察

其他会员也浏览了