Sunday, August 23, 2020

Flutter - Draw route on google map

The tracking feature of google map is most popular that display the current position of your ordered food and cab in a Mobile application. To implement this feature in the mobile app, we always need a google map that display the real position and path of the user.

Flutter google map route draw example

The Google Map is a web-based service that offers the geographical regions of the world. The Google map provides aerial and satellite views of many places. The google map route planner offers directions for drivers, bikers, walkers, and users of public transportation who want to take a trip from one place to another place. You have to just enter the address of your source and destination location. After that, you will see the right way to go there. The Google maps highlight the suggested route in a bright blue color and include other possible routes in gray. It is always safe to use a driving map when you are not aware of the place.

We have written some posts for the google map plugin and draw the route. But these depend on third-party plugins that is deprecated now. You will see, it has some limitations to work on UI design of google map. But if you are looking initial setup of google map in a Flutter application. You can read it from below:

Flutter - Google map plugin.
Flutter - How can draw route on google map between markers.

Now, Flutter team has created their own google map widget. The google map widget is very helpful to design inline google map in Flutter application. We can perform all the actions that we do with a normal widget. We have explained all aspect of this widget in separate post. If you want to learn all the feature of google map widget. You can read it from here: Flutter - Google map widget plugin example. 

 



In this post, we going to create a Flutter application to draw route on google map widget from your current position to searched destination address. To get the current location, we will use the GPS handler plugin and for a search destination address, we'll use places API. The final output of the project will be like below.

 

Creating a new project

1. Create a new Flutter project from File ⇒ New Flutter Project with your development IDE.
2. Add the plugin google_maps_webservice geolocator,   google_maps_flutter, and flutter_polyline_points as a dependency in the pubspec.yaml file. Once you do that, you need to run flutter packages.
3. In the next step, we have to manage the Google API key for both Android and iOS. We have explained steps to get API key here if you don't have it. Once you get it, put it in the respected location that's explained below.
  • Android: You have to put it in Flutter Android package in the application manifest (android/app/src/main/AndroidManifest.xml)

    <manifest ...
    <application ...
    <meta-data android:name="com.google.android.geo.API_KEY"
    android:value="YOUR ANDROID API KEY HERE"/>

    add necessary location permission, as follows:
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  •  iOS: You have to put it in Flutter iOS package in the application delegate (ios/Runner/AppDelegate.m), as follow:

    #include "AppDelegate.h"
    #include "GeneratedPluginRegistrant.h"
    // Add the GoogleMaps import.
    #import "GoogleMaps/GoogleMaps.h"
    @implementation AppDelegate
    - (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Add the following line with your API key.
    [GMSServices provideAPIKey:@"YOUR IOS API KEY HERE"];
    [GeneratedPluginRegistrant registerWithRegistry:self];
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
    }
    @end
    In the iOS package, you have to add a setting to the app’s Info.plist file (ios/Runner/Info.plist). We have to add a boolean property with the key io.flutter.embedded_views_preview and the value true.  

    <key>io.flutter.embedded_views_preview</key>
    <true/>
    Now add a NSLocationWhenInUseUsageDescription key to your Info.plist file. This will automatically prompt the user for permissions when the map tries to turn ON location.
4. Now start editing main.dart class as you can see, we have implemented our theme and displayed map screen by providing to home param.

import 'package:flutter/material.dart';
import 'package:flutterapp/map_screen.dart';

void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: const Color(0xFF02BB9F),
primaryColorDark: const Color(0xFF167F67),
accentColor: const Color(0xFF167F67),
),
home: MapScreen(),
));
}




5.
After that create map_screen.dart class to display the google map and input box to search destination address. We display google map widget by using GoogleMap that you can see below code snippet:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
import 'package:flutterapp/google_place_util.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class MapScreen extends StatefulWidget {
@override
_MapScreenState createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> implements GooglePlacesListener {

Map<PolylineId, Polyline> polylines = {};
List<LatLng> polylineCoordinates = [];
PolylinePoints polylinePoints = PolylinePoints();

Position _currentPosition;
String locationAddress = "Search destination";
GooglePlaces googlePlaces;
double _destinationLat;
double _destinationLng;

Completer<GoogleMapController> _controller = Completer();

CameraPosition _kGooglePlex = CameraPosition(
target: LatLng(0, 0),
zoom: 14.4746,
);


@override
void initState() {
super.initState();
googlePlaces = GooglePlaces(this);
_getCurrentLocation();
}

_getCurrentLocation() {
final Geolocator geolocator = Geolocator()..forceAndroidLocationManager;
geolocator
.getCurrentPosition(desiredAccuracy: LocationAccuracy.best)
.then((Position position) {
setState(() {
_currentPosition = position;
_updatePosition(_currentPosition);
});
}).catchError((e) {
print(e);
});
}

Future<void> _updatePosition(Position currentPosition) async {
final GoogleMapController controller = await _controller.future;
controller.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(
target: LatLng(currentPosition.latitude, currentPosition.longitude),
zoom: 14.4746,
)));
googlePlaces.updateLocation(currentPosition.latitude, currentPosition.longitude);
}

@override
Widget build(BuildContext context) {
var screenWidget = Stack(
children: <Widget>[
GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _kGooglePlex,
myLocationEnabled: true,
polylines: Set<Polyline>.of(polylines.values),
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
),

GestureDetector(
onTap: () {
googlePlaces.findPlace(context);
},
child: Container(
height: 50.0,
alignment: FractionalOffset.center,
margin: EdgeInsets.all(10.0),
padding: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0),
decoration: BoxDecoration(
color: const Color.fromRGBO(255, 255, 255, 1.0),
border: Border.all(color: const Color(0x33A6A6A6)),
borderRadius: BorderRadius.all(const Radius.circular(6.0)),
),
child: Row(
children: <Widget>[
Icon(Icons.search),
Flexible(
child: Container(
padding: EdgeInsets.only(right: 13.0),
child: Text(
locationAddress,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.black),
),
),
),
],
),
),
),
],
);

return Scaffold(
backgroundColor: const Color(0xFFA6AFAA),
appBar: AppBar(
title: Text(
"Google maps route",
textAlign: TextAlign.center,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
body: screenWidget,
);
}


@override
selectedLocation(double lat, double lng, String address) {
setState(() {
_destinationLat = lat;
_destinationLng = lng;
locationAddress = address;

});
_getPolyline();
}

_addPolyLine() {
polylines.clear();
PolylineId id = PolylineId("poly");
Polyline polyline = Polyline(
polylineId: id, color: Colors.red, points: polylineCoordinates);
polylines[id] = polyline;
setState(() {});
}

_getPolyline() async {
PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
"GOOGLE_KEY",
PointLatLng(_currentPosition.latitude, _currentPosition.longitude),
PointLatLng(_destinationLat, _destinationLng),
travelMode: TravelMode.driving
);
if (result.points.isNotEmpty) {
result.points.forEach((PointLatLng point) {
polylineCoordinates.add(LatLng(point.latitude, point.longitude));
});
}
_addPolyLine();
}
}



Let's try to understand the above code snippet:

  1. First of all the initState() method will invoke from the Flutter framwork. In this method, we calling _getCurrentLocation(). The _getCurrentLocation() will get the current user location if GPS is ON. So, make sure you have enabled the GPS. 
  2. When we get the user current location. The _updatePosition() method will automatically call and we display current location of user in the google map widget.
  3. When you will tap on seach box, the googlePlaces.findPlace(context); called. It'll will open a pop up, where your can search address. Once you search and tap on searched address, the selectedLocation() method will call.
  4. After that _getPolyline() will call to the get detail of route by using google api. Once it get the result, the _addPolyLine() parse the result and display on google map.



6
. Now create google_place_util.dart class. It'll search the address by using the google API and display on pop up list. When you select any visible address. It'll close and move to main screen of app.

import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutterapp/flutter_google_places_autocomplete.dart';
import 'package:google_maps_webservice/places.dart';

class GooglePlaces {
final homeScaffoldKey = GlobalKey<ScaffoldState>();
final searchScaffoldKey = GlobalKey<ScaffoldState>();
GoogleMapsPlaces _places = GoogleMapsPlaces(apiKey:"GOOGLE_KEY");
Location location;
GooglePlacesListener _mapScreenState;

GooglePlaces(this._mapScreenState);

Future findPlace(BuildContext context) async {
Prediction p = await showGooglePlacesAutocomplete(
context: context,
location: location,
apiKey: "GOOGLE_KEY",
onError: (res) {
homeScaffoldKey.currentState
.showSnackBar( SnackBar(content: Text(res.errorMessage)));
},
);

displayPrediction(p, homeScaffoldKey.currentState);
}

Future<Null> displayPrediction(Prediction p, ScaffoldState scaffold) async {
if (p != null) {
// get detail (lat/lng)
PlacesDetailsResponse detail =
await _places.getDetailsByPlaceId(p.placeId);
final lat = detail.result.geometry.location.lat;
final lng = detail.result.geometry.location.lng;
_mapScreenState.selectedLocation(
lat, lng, detail.result.formattedAddress);
}
}

void updateLocation(double lat, double long) {
location = Location(lat, long);
}
}

abstract class GooglePlacesListener {
selectedLocation(double lat, double long, String address);
}



7. Now create flutter_google_place_autocomplete.dart class. It is used inside of above class that actually manage and display search address result in the drop down list.

library flutter_google_places_autocomplete.src;

import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_webservice/places.dart';

class GooglePlacesAutocompleteWidget extends StatefulWidget {

final String apiKey;
final String hint;
final Location location;
final num offset;
final num radius;
final String language;
final List<String> types;
final List<Component> components;
final bool strictbounds;
final ValueChanged<PlacesAutocompleteResponse> onError;

GooglePlacesAutocompleteWidget(
{@required this.apiKey,
this.hint = "Search",
this.offset,
this.location,
this.radius,
this.language,
this.types,
this.components,
this.strictbounds,
this.onError,
Key key})
: super(key: key);

@override
State<GooglePlacesAutocompleteWidget> createState() {
return _GooglePlacesAutocompleteOverlayState();
}

static GooglePlacesAutocompleteState of(BuildContext context) => context
.ancestorStateOfType(const TypeMatcher<GooglePlacesAutocompleteState>());
}

class _GooglePlacesAutocompleteOverlayState
extends GooglePlacesAutocompleteState {
@override
Widget build(BuildContext context) {
final header = Column(children: <Widget>[
Material(

child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
IconButton(
color: Colors.black45,
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.pop(context);
},
),
Expanded(
child: Padding(
child: _textField(),
padding: const EdgeInsets.only(right: 8.0),
)),
],
)),
Divider(
//height: 1.0,
)
]);

var body;

if (query.text.isEmpty ||
response == null ||
response.predictions.isEmpty) {
body = Material(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(2.0),
bottomRight: Radius.circular(2.0)),
);
} else {
body = SingleChildScrollView(
child: Material(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(2.0),
bottomRight: Radius.circular(2.0)),
color: Colors.white,
child: ListBody(
children: response.predictions
.map((p) => PredictionTile(
prediction: p, onTap: Navigator.of(context).pop))
.toList())));
}

final container = Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 30.0),
child: Stack(children: <Widget>[
header,
Padding(padding: EdgeInsets.only(top: 48.0), child: body),
]));

if (Platform.isIOS) {
return Padding(
padding: EdgeInsets.only(top: 8.0), child: container);
}
return container;
}

Widget _textField() => TextField(
controller: query,
autofocus: true,
decoration: InputDecoration(
hintText: widget.hint,
hintStyle: TextStyle(color: Colors.black54, fontSize: 16.0),
border: null),
onChanged: search,
);
}

class GooglePlacesAutocompleteResult extends StatefulWidget {
final ValueChanged<Prediction> onTap;

GooglePlacesAutocompleteResult({this.onTap});

@override
_GooglePlacesAutocompleteResult createState() =>
_GooglePlacesAutocompleteResult();
}

class _GooglePlacesAutocompleteResult
extends State<GooglePlacesAutocompleteResult> {
@override
Widget build(BuildContext context) {
final state = GooglePlacesAutocompleteWidget.of(context);
assert(state != null);

if (state.query.text.isEmpty ||
state.response == null ||
state.response.predictions.isEmpty) {
final children = <Widget>[];

return Stack(children: children);
}
return PredictionsListView(
predictions: state.response.predictions, onTap: widget.onTap);
}
}

class PredictionsListView extends StatelessWidget {
final List<Prediction> predictions;
final ValueChanged<Prediction> onTap;

PredictionsListView({@required this.predictions, this.onTap});

@override
Widget build(BuildContext context) {
return ListView(
children: predictions
.map((Prediction p) =>
PredictionTile(prediction: p, onTap: onTap))
.toList());
}
}

class PredictionTile extends StatelessWidget {
final Prediction prediction;
final ValueChanged<Prediction> onTap;

PredictionTile({@required this.prediction, this.onTap});

@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(Icons.location_on),
title: Text(prediction.description),
onTap: () {
if (onTap != null) {
onTap(prediction);
}
},
);
}
}

Future<Prediction> showGooglePlacesAutocomplete(
{@required BuildContext context,
@required String apiKey,
String hint = "Search",
num offset,
Location location,
num radius,
String language,
List<String> types,
List<Component> components,
bool strictbounds,
ValueChanged<PlacesAutocompleteResponse> onError}) {
final builder = (BuildContext ctx) => GooglePlacesAutocompleteWidget(
apiKey: apiKey,
language: language,
components: components,
types: types,
location: location,
radius: radius,
strictbounds: strictbounds,
offset: offset,
hint: hint,
onError: onError,
);

return showDialog(context: context, builder: builder);
}

abstract class GooglePlacesAutocompleteState
extends State<GooglePlacesAutocompleteWidget> {
TextEditingController query;
PlacesAutocompleteResponse response;
GoogleMapsPlaces _places;
bool searching;

@override
void initState() {
super.initState();
query = TextEditingController(text: "");
_places = GoogleMapsPlaces(apiKey:widget.apiKey);
searching = false;
}

Future<Null> doSearch(String value) async {
if (mounted && value.isNotEmpty) {
setState(() {
searching = true;
});

final res = await _places.autocomplete(value,
offset: widget.offset,
location: widget.location,
radius: widget.radius,
language: widget.language,
types: widget.types,
components: widget.components,
strictbounds: widget.strictbounds);

if (res.errorMessage?.isNotEmpty == true ||
res.status == "REQUEST_DENIED") {
onResponseError(res);
} else {
onResponse(res);
}
} else {
onResponse(null);
}
}

Timer _timer;

Future<Null> search(String value) async {
_timer?.cancel();
_timer = Timer(const Duration(milliseconds: 300), () {
_timer.cancel();
doSearch(value);
});
}

@override
void dispose() {
_timer?.cancel();
_places.dispose();
super.dispose();
}

@mustCallSuper
void onResponseError(PlacesAutocompleteResponse res) {
if (mounted) {
if (widget.onError != null) {
widget.onError(res);
}
setState(() {
response = null;
searching = false;
});
}
}

@mustCallSuper
void onResponse(PlacesAutocompleteResponse res) {
if (mounted) {
setState(() {
response = res;
searching = false;
});
}
}
}

 

 

If you have followed the above post carefully. You will see Flutter application drawing route on google map as shown above. But if you are facing any problem to implement google route draw and you have any quires, please feel free to ask it from comment section below.

Share:

Youtube

We're Social

Flutter - Expendable list with ExpansionTile and ExpansionPanel

In mobile application, if you want to show items in a list with expandable feature. The expandable view is a way that show items in a vertic...