Mastering Google Maps in Flutter: Building a Comprehensive Navigation App from Scratch

Mastering Google Maps in Flutter: Building a Comprehensive Navigation App from Scratch

In the ever-expanding realm of the digital world, location-based services have cemented their place as indispensable tools in our everyday lives. Whether you need to pinpoint the closest eatery, track a parcel, or navigate a road trip, map applications are a lifeline. Google Maps is a foremost example of such a utility, and in this blog post, we’ll be crafting a similar application using Flutter. We’ll delve into the incorporation of map views, geocoding, and routing. So, get ready for an exciting journey into the world of Flutter mapping!

Create Flutter Project

Before diving into the deep end, ensure Flutter and Dart is installed on your system. If not, please refer to the official Flutter installation guide. After setting up Flutter and Dart, we’ll kick start the process by creating a new Flutter application.

flutter create flutter_maps
cd flutter_maps

Add the Google Maps Flutter Package

To display Google Maps in our app, we need to integrate the google_maps_flutter package to our pubspec.yaml file.

google_maps_flutter: ^2.2.6

Remember to install it by running flutter pub get.

Setting Up

You’ll need a Google Maps API Key to utilize the Google Maps services. Follow the official guide to procure your key. Once you’ve obtained it, it’s time to add it to your app.

For Android, locate the AndroidManifest.xml file and insert the following line within the <application> tag:

<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_GOOGLE_MAP_API_KEY"/>

To request location access in the app, add the following permission within the same file inside the <manifest> tag:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

For iOS, locate AppDelegate.swift and add the following line at the commencement of the application function:

GMSServices.provideAPIKey("YOUR_GOOGLE_MAP_API_KEY")

To request location permission, insert the following in the same Info.plist file:

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>

With this, we’ve completed the setup for both platforms in Flutter.

Integrating the Google Maps

With the Google Maps package installed and the API key configured, it’s time to exhibit the map. We’ll initiate by incorporating the GoogleMap widget in our main.dart file. This widget accepts a cameraPosition parameter that determines the initial location and zoom level of the map.

GoogleMap(
    markers: Set<Marker>.from(markers),
    initialCameraPosition: _initialLocation,
    myLocationEnabled: true,
    myLocationButtonEnabled: false,
    mapType: MapType.normal,
    zoomGesturesEnabled: true,
    zoomControlsEnabled: false,
    polylines: Set<Polyline>.of(polylines.values),
    onMapCreated: (GoogleMapController controller) {
        mapController = controller;
    },
),

Let’s understand the parameters used in the Google Maps widget:

  • initialCameraPosition: This compulsory parameter loads the map view on the initial launch.
  • myLocationEnabled: This displays your current location on the map as a blue dot.
  • myLocationButtonEnabled: This button centralizes the user’s location in the camera view.
  • mapType: This designates the displayed map type (normal, satellite, hybrid, or terrain).
  • zoomGesturesEnabled: This determines whether the map view should respond to zoom gestures.
  • zoomControlsEnabled: This decides whether to showcase zoom controls (only applicable for Android).
  • onMapCreated: This is a callback for when the map is ready for use.

In this case, we’ve set myLocationButtonEnabled and zoomControlsEnabled parameters to false. The mapController will be utilized to manipulate the camera position of the map view.

Move to a new position

To move to a new position, you can employ the following code snippet:

mapController.animateCamera(
    CameraUpdate.newCameraPosition(
        CameraPosition(
            target: LatLng(
                _currentPosition.latitude,
                _currentPosition.longitude,
            ),
            zoom: 18.0,
        ),
    ),
);

In this code, we’ve only defined the target and zoom properties of the CameraPosition widget. There are two more properties, bearing and tilt, which can be used similarly.

In the target property, you pass the latitude and longitude of the position you want to relocate to.

In our case, we require the Current Location button to shift the camera view to the user’s current location. Let’s explore how to achieve that.

Fetching current location

The Geolocator plugin for Flutter can assist you in fetching the user’s current location.

Add it to your pubspec.yaml file:

codegeolocator: 9.0.2

Now, let’s walk through this step by step:

  1. Define a variable to store the current location:
// For storing the current position 
Position _currentPosition;
  1. Get the user’s current location:
// Method for retrieving the current location 
_getCurrentLocation() async { 
    await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high) 
        .then((Position position) async { 
            setState(() { 
                // Store the position in the variable 
                _currentPosition = position; 
                print('CURRENT POS: $_currentPosition'); 

                // For moving the camera to current location 
                mapController.animateCamera( 
                    CameraUpdate.newCameraPosition( 
                        CameraPosition( 
                            target: LatLng(position.latitude, position.longitude), 
                            zoom: 18.0, 
                        ), 
                    ), 
                ); 
            }); 
            await _getAddress(); 
        }).catchError((e) { 
            print(e); 
        }); 
}
  1. Add this method to the initState to fetch the user’s current location as soon as the app launches and to move the camera to the detected location.
@Override 
void initState() { 
    super.initState(); 
    _getCurrentLocation(); 
}
  1. Also, pass the latitude and longitude to the onTap method of the custom button for fetching the current location.

After completing these steps, the camera will automatically move to the detected location when the app launches.

Geocoding

Once you have the initial app configured and functioning, you can proceed to add two locations and determine the optimal route between them on the map. For this, you will require the latitude and longitude of these locations. As figuring out geographic coordinates can be quite complex, we will employ geocoding.

Geocoding is a technique that allows you to convert the address of a location into coordinates (latitude and longitude) and vice versa.

Include the geocoding Flutter package in your pubspec.yaml file:

geocoding: ^2.0.0

To begin, you should introduce the addresses as user inputs using TextField.

If you wish to display the address of the starting location, you need to define a TextEditingController for the starting address and update the text as soon as the address of the user’s current location is retrieved.

final startAddressController = TextEditingController();

You can retrieve the address using the following method:

// Method for retrieving the address
_getAddress() async {
  try {
    // Places are retrieved using the coordinates
    List<Placemark> p = await placemarkFromCoordinates(
        _currentPosition.latitude, _currentPosition.longitude);

    // Taking the most probable result
    Placemark place = p[0];

    setState(() {

      // Structuring the address
      _currentAddress =
          "${place.name}, ${place.locality}, ${place.postalCode}, ${place.country}";
      
      // Update the text of the TextField
      startAddressController.text = _currentAddress;

      // Setting the user's present location as the starting address
      _startAddress = _currentAddress;
    });
  } catch (e) {
    print(e);
  }
}

A similar method can be used to retrieve the coordinates from the starting address and the destination address. You will need these coordinates to plot a route on the map and to place markers.

// Retrieving placemarks from addresses
List<Location> startPlacemark = await locationFromAddress(_startAddress);
List<Location> destinationPlacemark = await locationFromAddress(_destinationAddress);

// Storing latitude & longitude of start and destination location
double startLatitude = startPlacemark[0].latitude;
double startLongitude = startPlacemark[0].longitude;
double destinationLatitude = destinationPlacemark[0].latitude;
double destinationLongitude = destinationPlacemark[0].longitude;

Placing Markers

To mark the start and end points of the route on the map, you need to place markers on those locations. You can use the coordinates retrieved in the previous step for this.

Firstly, define a Set to store the markers:

Set<Marker> markers = {};

Next, create the markers. You can use the Marker class for this:

String startCoordinatesString = '($startLatitude, $startLongitude)'; 
String destinationCoordinatesString = '($destinationLatitude, $destinationLongitude)';

// Start Location Marker
Marker startMarker = Marker(
  markerId: MarkerId(startCoordinatesString),
  position: LatLng(startLatitude, startLongitude),
  infoWindow: InfoWindow(
    title: 'Start $startCoordinatesString',
    snippet: _startAddress,
  ),
  icon: BitmapDescriptor.defaultMarker,
);

// Destination Location Marker
Marker destinationMarker = Marker(
  markerId: MarkerId(destinationCoordinatesString),
  position: LatLng(destinationLatitude, destinationLongitude),
  infoWindow: InfoWindow(
    title: 'Destination $destinationCoordinatesString',
    snippet: _destinationAddress,
  ),
  icon: BitmapDescriptor.defaultMarker,
);

After creating the markers, add them to the Set:

markers.add(startMarker); 
markers.add(destinationMarker);

Then, display the markers on the map. You can do this by adding the markers property to the GoogleMap widget:

GoogleMap(
  markers: Set<Marker>.from(markers),
  // ... 
),

There may be instances when you add multiple markers, but only one is visible on the map. This could be because the other markers are outside the current view. You can zoom out to see if that’s the case. Alternatively, you can use the following code to adjust the map view to accommodate all the markers:

double miny = (startLatitude <= destinationLatitude)
    ? startLatitude
    : destinationLatitude;
double minx = (startLongitude <= destinationLongitude)
    ? startLongitude
    : destinationLongitude;
double maxy = (startLatitude <= destinationLatitude)
    ? destinationLatitude
    : startLatitude;
double maxx = (startLongitude <= destinationLongitude)
    ? destinationLongitude
    : startLongitude;

double southWestLatitude = miny;
double southWestLongitude = minx;

double northEastLatitude = maxy;
double northEastLongitude = maxx;

mapController.animateCamera(
  CameraUpdate.newLatLngBounds(
    LatLngBounds(
      northeast: LatLng(northEastLatitude, northEastLongitude),
      southwest: LatLng(southWestLatitude, southWestLongitude),
    ),
    100.0,
  ),
);

Drawing a Route

You can use the Polyline class to draw a route on Google Maps. This class represents a list of points, with line segments drawn between consecutive points. You can use the flutter_polyline_points package for this purpose. This package uses the Directions API that you enabled earlier.

Add the package to your pubspec.yaml file:

flutter_polyline_points: ^1.0.0

Next, define some variables:

// Object for PolylinePoints 
late PolylinePoints polylinePoints; 

// List of coordinates to join 
List<LatLng> polylineCoordinates = []; 

// Map storing polylines created by connecting two points 
Map<PolylineId, Polyline> polylines = {};

Then, define a method for creating the polylines:

_createPolylines( 
  double startLatitude, 
  double startLongitude, 
  double destinationLatitude, 
  double destinationLongitude,
) async { 
    // Initializing PolylinePoints
    polylinePoints = PolylinePoints();

    // Generating the list of coordinates to be used for
    // drawing the polylines
    PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
        "YOUR_GOOGLE_MAP_API_KEY", // Google Maps API Key
        PointLatLng(startLatitude, startLongitude),
        PointLatLng(destinationLatitude, destinationLongitude),
        travelMode: TravelMode.transit,
      );

      // Adding the coordinates to the list
      if (result.points.isNotEmpty) {
        result.points.forEach((PointLatLng point) {
          polylineCoordinates.add(LatLng(point.latitude, point.longitude));
        });
      }

      // Defining an ID
      PolylineId id = PolylineId('poly');

      // Initializing Polyline
      Polyline polyline = Polyline(
        polylineId: id,
        color: Colors.red,
        points: polylineCoordinates,
        width: 3,
      );

      // Adding the polyline to the map
      polylines[id] = polyline;
}

After defining the method, you can add the polylines to the GoogleMap widget:

GoogleMap(
  polylines: Set<Polyline>.of(polylines.values),
  // ...
),

Calculating Distance

Finally, you might want to calculate the distance between the two locations. For this, you can use the bearingBetween method from the geolocator package:

double distanceInMeters = await Geolocator.bearingBetween(
  startLatitude,
  startLongitude,
  destinationLatitude,
  destinationLongitude,
);

However, this method calculates the distance as a straight line between the two points, not the actual route. To calculate the distance of the route, you can break it into several small segments, calculate the distance of each segment, and then sum them up:

import 'dart:math' show cos, sqrt, asin;

double _coordinateDistance(lat1, lon1, lat2, lon2) {
  var p = 0.017453292519943295;
  var c = cos;
  var a = 0.5 -
      c((lat2 - lat1) * p) / 2 +
      c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2;
  return 12742 * asin(sqrt(a));
}

double totalDistance = 0.0;

// Calculating the total distance by adding the distance
// between small segments
for (int i = 0; i < polylineCoordinates.length - 1; i++) {
  totalDistance += _coordinateDistance(
    polylineCoordinates[i].latitude,
    polylineCoordinates[i].longitude,
    polylineCoordinates[i + 1].latitude,
    polylineCoordinates[i + 1].longitude,
  );
}

// Storing the calculated total distance of the route
setState(() {
  _placeDistance = totalDistance.toStringAsFixed(2);
  print('DISTANCE: $_placeDistance km');
});

Finally, show the calculated distance on the UI:

Visibility(
  visible: _placeDistance == null ? false : true,
  child: Text(
    'DISTANCE: $_placeDistance km',
    style: TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.bold,
    ),
  ),
),

The final application should now successfully display the route between two locations, along with the distance of the route.

Output

Conclusion

In conclusion, this guide has walked you through integrating Google Maps into a Flutter application, allowing users to input locations, visualize routes, and calculate distances. We’ve used key tools like the Maps SDK, Directions API, and Geolocator package. For more customization, consider exploring additional features such as real-time navigation and traffic conditions. The full code for this project can be found on GitHub. Always remember to respect user privacy and test your application thoroughly. Happy Mapping!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top