Sunday, August 26, 2018

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

Route on google map is the easiest way to reach somewhere. Just enter the address of your source and destination point. After that, you will see the right way to go there and you can control the route map.  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.



 
If you looking route draw on google map widget. Then you should follow the updated post Flutter - Draw route on google map .
 
The following post has been deprecated
 
In this post, we'll create a Flutter application for draw route on google map from your current position to the 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.


In our previous post, we have seen the basic setup and implementation of google map plugin in the Flutter Application. If you have never used google map in Flutter. So, you should read our previous post. In this post, we'll not discuss the basic part of google map. We assuming here, you have a google map key with enabled google map API for Android and Ios.

So, let start it and see, how we can draw the route on google map in a Flutter Application. 



Creating a new Project
1. Create a new project from File ⇒ New Flutter Project with your development IDE.

2. Include required following dependency in the pubspec.yaml.
location: ^1.4.1
map_view: "^0.0.14"
google_maps_webservice: ^0.0.6
  • location: ^1.4.1 will manage GPS of the device and get current user position.
  • map_view: "^0.0.14" will display google map. We have discussed it in our previous post
  • We using google_maps_webservice: ^0.0.6 for get google places. By using this, we'll able to get destination Address position.   
3. Open main.dart file and edit it. As we have set our theme and change debug banner property of Application.
main.dart
import 'package:flutter/material.dart'; import 'package:flutter_google_map_route/map_screen.dart'; import 'package:map_view/map_view.dart'; void main() { MapView.setApiKey("Google_API_Key"); runApp(new MaterialApp( debugShowCheckedModeBanner: false, theme: new ThemeData( primaryColor: const Color(0xFF02BB9F), primaryColorDark: const Color(0xFF167F67), accentColor: const Color(0xFF167F67), ), home: new MapScreen(), )); }

4. After that create home screen map_screen.dart of the application and start designing it. As you can see above, we have created a destination address widget for search destination location that we'll use for draw route on google map.
map_screen.dart
new GestureDetector( onTap: () { googlePlaces.findPlace(context); }, child: new Container( alignment: FractionalOffset.center, margin: EdgeInsets.all(10.0), padding: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0), decoration: new BoxDecoration( color: const Color.fromRGBO(255, 255, 255, 1.0), border: Border.all(color: const Color(0x33A6A6A6)), borderRadius: new BorderRadius.all(const Radius.circular(6.0)), ), child: new Row( children: <Widget>[ new Icon(Icons.search), new Flexible( child: new Container( padding: new EdgeInsets.only(right: 13.0), child: new Text( locationAddress, overflow: TextOverflow.ellipsis, style: new TextStyle(color: Colors.black), ), ), ), ], ), ), ),
now create another widget that will display static google map image.
map_screen.dart
new Container( height: 230.0, child: new Stack( children: <Widget>[ new Center( child: Container( child: new Text( "Google Map Box", textAlign: TextAlign.center, ), padding: const EdgeInsets.all(20.0), ), ), new GestureDetector( onTap: () => mapUtil.showMap(), child: new Center( child: new Image.network(mapUtil.getStaticMap().toString()), ), ), ], ), ),
as you can see the final map_screen.dart file below. We have used a Text and Button to get route steps from Google API.
map_screen.dart
import 'package:flutter/material.dart'; import 'package:flutter_google_map_route/progress_hud.dart'; import 'package:flutter_google_map_route/utils/google_place_util.dart'; import 'package:flutter_google_map_route/utils/map_util.dart'; import 'package:map_view/map_view.dart'; class MapScreen extends StatefulWidget { @override _MapScreenState createState() => new _MapScreenState(); } class _MapScreenState extends State<MapScreen> implements ScreenListener, GooglePlacesListener { MapUtil mapUtil; String locationAddress = "Search destination"; String myLocation = ""; GooglePlaces googlePlaces; bool _isLoading = false; double _destinationLat; double _destinationLng; @override void initState() { super.initState(); mapUtil = new MapUtil(this); mapUtil.init(); googlePlaces = new GooglePlaces(this); } @override Widget build(BuildContext context) { var screenWidget = new Column( children: <Widget>[ new GestureDetector( onTap: () { googlePlaces.findPlace(context); }, child: new Container( alignment: FractionalOffset.center, margin: EdgeInsets.all(10.0), padding: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0), decoration: new BoxDecoration( color: const Color.fromRGBO(255, 255, 255, 1.0), border: Border.all(color: const Color(0x33A6A6A6)), borderRadius: new BorderRadius.all(const Radius.circular(6.0)), ), child: new Row( children: <Widget>[ new Icon(Icons.search), new Flexible( child: new Container( padding: new EdgeInsets.only(right: 13.0), child: new Text( locationAddress, overflow: TextOverflow.ellipsis, style: new TextStyle(color: Colors.black), ), ), ), ], ), ), ), new Container( height: 230.0, child: new Stack( children: <Widget>[ new Center( child: Container( child: new Text( "Google Map Box", textAlign: TextAlign.center, ), padding: const EdgeInsets.all(20.0), ), ), new GestureDetector( onTap: () => mapUtil.showMap(), child: new Center( child: new Image.network(mapUtil.getStaticMap().toString()), ), ), ], ), ), new Container( margin: new EdgeInsets.fromLTRB(0.0, 20.0, 0.0, 0.0), padding: new EdgeInsets.only(top: 10.0), child: new Text( myLocation, style: new TextStyle(fontWeight: FontWeight.bold), ), ), new GestureDetector( onTap: () => getMapRoute(), child: new Container( margin: EdgeInsets.fromLTRB(30.0, 30.0, 30.0, 0.0), padding: EdgeInsets.all(15.0), alignment: FractionalOffset.center, decoration: new BoxDecoration( color: const Color(0xFFFFD900), borderRadius: new BorderRadius.all(const Radius.circular(6.0)), ), child: Text( "Draw Route", style: new TextStyle( color: const Color(0xFF28324E), fontSize: 20.0, fontWeight: FontWeight.bold), ), ), ), ], ); return new Scaffold( backgroundColor: const Color(0xFFA6AFAA), appBar: AppBar( title: new Text( "Google maps route", textAlign: TextAlign.center, style: new TextStyle( fontWeight: FontWeight.bold, color: Colors.white, ), ), ), body: ProgressHUD( child: new SingleChildScrollView( child: screenWidget, ), inAsyncCall: _isLoading, opacity: 0.0, ), ); } Widget getTextField( String inputBoxName, TextEditingController inputBoxController) { var loginBtn = new Padding( padding: const EdgeInsets.all(5.0), child: new TextFormField( controller: inputBoxController, decoration: new InputDecoration( hintText: inputBoxName, ), ), ); return loginBtn; } Widget getButton(String buttonLabel, EdgeInsets margin) { var staticMapBtn = new Container( margin: margin, padding: EdgeInsets.all(8.0), alignment: FractionalOffset.center, decoration: new BoxDecoration( color: const Color(0xFF167F67), border: Border.all(color: const Color(0xFF28324E)), borderRadius: new BorderRadius.all(const Radius.circular(6.0)), ), child: new Text( buttonLabel, style: new TextStyle( color: const Color(0xFFFFFFFF), fontSize: 20.0, fontWeight: FontWeight.w300, letterSpacing: 0.3, ), ), ); return staticMapBtn; } updateStaticMap() { setState(() {}); } @override updateScreen(Location location) { myLocation = "You are at: " + location.latitude.toString() + ", " + location.longitude.toString(); googlePlaces.updateLocation(location.latitude, location.longitude); setState(() {}); } @override selectedLocation(double lat, double lng, String address) { setState(() { _destinationLat = lat; _destinationLng = lng; locationAddress = address; }); } getMapRoute() { setState(() { _isLoading = true; }); mapUtil.getDirectionSteps(_destinationLat, _destinationLng); } @override dismissLoader() { setState(() { _isLoading = false; }); } } abstract class ScreenListener { updateScreen(Location location); dismissLoader(); }
in the above class, we have created instances of the util class to manage map and GPS in the init method. We have a ScreenListener abstract class that will update map_screen.dart widget.




5. Now create flutter_google_places_autocomplete.dart file. It will manage destination widget dialog on the map_screen.dart.
flutter_google_places_autocomplete.dart
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 new _GooglePlacesAutocompleteOverlayState(); } static GooglePlacesAutocompleteState of(BuildContext context) => context .ancestorStateOfType(const TypeMatcher<GooglePlacesAutocompleteState>()); } class _GooglePlacesAutocompleteOverlayState extends GooglePlacesAutocompleteState { @override Widget build(BuildContext context) { final header = new Column(children: <Widget>[ new Material( child: new Row( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ new IconButton( color: Colors.black45, icon: new Icon(Icons.arrow_back), onPressed: () { Navigator.pop(context); }, ), new Expanded( child: new Padding( child: _textField(), padding: const EdgeInsets.only(right: 8.0), )), ], )), new Divider( //height: 1.0, ) ]); var body; if (query.text.isEmpty || response == null || response.predictions.isEmpty) { body = new Material( color: Colors.white, borderRadius: new BorderRadius.only( bottomLeft: new Radius.circular(2.0), bottomRight: new Radius.circular(2.0)), ); } else { body = new SingleChildScrollView( child: new Material( borderRadius: new BorderRadius.only( bottomLeft: new Radius.circular(2.0), bottomRight: new Radius.circular(2.0)), color: Colors.white, child: new ListBody( children: response.predictions .map((p) => new PredictionTile( prediction: p, onTap: Navigator.of(context).pop)) .toList()))); } final container = new Container( margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 30.0), child: new Stack(children: <Widget>[ header, new Padding(padding: new EdgeInsets.only(top: 48.0), child: body), ])); if (Platform.isIOS) { return new Padding( padding: new EdgeInsets.only(top: 8.0), child: container); } return container; } Widget _textField() => new TextField( controller: query, autofocus: true, decoration: new InputDecoration( hintText: widget.hint, hintStyle: new 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() => new _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 new Stack(children: children); } return new 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 new ListView( children: predictions .map((Prediction p) => new 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 new ListTile( leading: new Icon(Icons.location_on), title: new 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) => new 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 = new TextEditingController(text: ""); _places = new GoogleMapsPlaces(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 = new 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; }); } } }



6. After that create some util classes gps_util.dart, map_util.dart and google_place_util.dart.
  •  gps_util.dart  will get your current position and update to another aspect of the app.
gps_util.dart
import 'dart:async'; import 'package:flutter/services.dart'; import 'package:location/location.dart'; class GpgUtils { GpsUtilListener listener; Map<String, double> _startLocation; Map<String, double> _currentLocation; StreamSubscription<Map<String, double>> _locationSubscription; Location _location = new Location(); bool _permission = false; String error; bool currentWidget = true; GpgUtils(this.listener); void init() { initPlatformState(); _locationSubscription = _location.onLocationChanged().listen((Map<String,double> result) { _currentLocation = result; listener.onLocationChange(_currentLocation); }); } // Platform messages are asynchronous, so we initialize in an async method. initPlatformState() async { Map<String, double> location; // Platform messages may fail, so we use a try/catch PlatformException. try { _permission = await _location.hasPermission(); location = await _location.getLocation(); error = null; } on PlatformException catch (e) { if (e.code == 'PERMISSION_DENIED') { error = 'Permission denied'; } else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') { error = 'Permission denied - please ask the user to enable it from the app settings'; } location = null; } _startLocation = location; listener.onLocationChange(_startLocation); } } abstract class GpsUtilListener { onLocationChange(Map<String, double> location); }
  • map_util.dart will manage google map methods. Here, we have created a method getDirectionSteps. It will take a source and destination param and get google route map steps by using google map API. Before using it, make sure you have enabled it from google console as discussed above.
map_util.dart
import 'package:flutter/material.dart'; import 'package:flutter_google_map_route/utils/gps_util.dart'; import 'package:flutter_google_map_route/map_screen.dart'; import 'package:flutter_google_map_route/model/route.dart'; import 'package:flutter_google_map_route/network/networ_util.dart'; import 'package:map_view/map_view.dart'; import 'package:map_view/polyline.dart'; class MapUtil implements GpsUtilListener { var staticMapProvider; CameraPosition cameraPosition; var location = new Location(0.0, 0.0); var zoomLevel = 18.0; MapView mapView; NetworkUtil network = new NetworkUtil(); GpgUtils gpgUtils; ScreenListener _screenListener; List<Location> ccc; MapUtil(this._screenListener); init() { mapView = new MapView(); gpgUtils = new GpgUtils(this); gpgUtils.init(); staticMapProvider = new StaticMapProvider("google_api_key"); } getDirectionSteps(double destinationLat, double destinationLng) { network .get("origin=" + location.latitude.toString() + "," + location.longitude.toString() + "&destination=" + destinationLat.toString() + "," + destinationLng.toString() + "&key=google_api_key") .then((dynamic res) { List<Steps> rr = res; print(res.toString()); ccc = new List(); for (final i in rr) { ccc.add(i.startLocation); ccc.add(i.endLocation); } mapView.onMapReady.listen((_) { mapView.setMarkers(getMarker(location.latitude,location.longitude,destinationLat,destinationLng)); mapView.addPolyline(new Polyline("12", ccc, width: 15.0)); }); _screenListener.dismissLoader(); showMap(); }).catchError((Exception error) => _screenListener.dismissLoader()); } List<Marker> getMarker(double scrLat,double scrLng,double desLat,double desLng) { List<Marker> markers = <Marker>[ new Marker("1", "My Location", scrLat, scrLng, color: Colors.amber), new Marker("2", "Destination", desLat, desLng, color: Colors.red), ]; return markers; } Uri getStaticMap() { return staticMapProvider.getStaticUri(getMyLocation(), zoomLevel.toInt(), height: 400, width: 900); } Location getMyLocation() { return location; } CameraPosition getCamera() { cameraPosition = new CameraPosition(getMyLocation(), zoomLevel); return cameraPosition; } showMap() { mapView.show( new MapOptions( mapViewType: MapViewType.normal, initialCameraPosition: getCamera(), showUserLocation: true, title: "Draw route"), toolbarActions: [new ToolbarAction("Close", 1)]); mapView.onToolbarAction.listen((id) { if (id == 1) { mapView.dismiss(); } }); } updateLocation(Location location) { this.location = location; } updateZoomLevel(double zoomLevel) { this.zoomLevel = zoomLevel; } @override onLocationChange(Map<String, double> currentLocation) { location = new Location(currentLocation["latitude"], currentLocation["longitude"]); _screenListener.updateScreen(location); } cameraUpdate(CameraPosition cameraPosition) { print("campera position changed $location"); } void manageMapProperties() { mapView.zoomToFit(padding: 100); mapView.onLocationUpdated.listen((location) => updateLocation(location)); mapView.onTouchAnnotation.listen((marker) => print("marker tapped")); mapView.onMapTapped.listen((location) => updateLocation(location)); mapView.onCameraChanged .listen((cameraPosition) => cameraUpdate(cameraPosition)); } }

  • google_place_util.dart  will get google place list from the server.
google_place_util.dart
import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_google_map_route/flutter_google_places_autocomplete.dart'; import 'package:google_maps_webservice/places.dart'; class GooglePlaces { final homeScaffoldKey = new GlobalKey<ScaffoldState>(); final searchScaffoldKey = new GlobalKey<ScaffoldState>(); GoogleMapsPlaces _places = new GoogleMapsPlaces("google_api_key"); Location location; GooglePlacesListener _mapScreenState; GooglePlaces(this._mapScreenState); Future findPlace(BuildContext context) async { Prediction p = await showGooglePlacesAutocomplete( context: context, location: location, apiKey: "google_api_key", onError: (res) { homeScaffoldKey.currentState .showSnackBar(new SnackBar(content: new 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 = new Location(lat, long); } } abstract class GooglePlacesListener { selectedLocation(double lat, double long, String address); }

7. To get route steps, we using Google API in this project. For fetch steps, we have created another util class network_util.dart.
network_util.dart
import 'dart:async'; import 'dart:convert'; import 'package:flutter_google_map_route/model/route.dart'; import 'package:http/http.dart' as http; class NetworkUtil { static final BASE_URL = "https://maps.googleapis.com/maps/api/directions/json?"; static NetworkUtil _instance = new NetworkUtil.internal(); NetworkUtil.internal(); factory NetworkUtil() => _instance; final JsonDecoder _decoder = new JsonDecoder(); Future<dynamic> get(String url) { return http.get(BASE_URL + url).then((http.Response response) { String res = response.body; int statusCode = response.statusCode; print("API Response: " + res); if (statusCode < 200 || statusCode > 400 || json == null) { res = "{\"status\":" + statusCode.toString() + ",\"message\":\"error\",\"response\":" + res + "}"; throw new Exception(res); } List<Steps> steps; try { steps = parseSteps(_decoder.convert(res)["routes"][0]["legs"][0]["steps"]); } catch (e) { throw new Exception(res); } return steps; }); } List<Steps> parseSteps(final responseBody) { var list = responseBody.map<Steps>((json) => new Steps.fromJson(json)).toList(); return list; } }

8. To parse the response of Google API,  we have created a POJO model class.

steps.dart
import 'package:map_view/location.dart'; class Steps { Location startLocation; Location endLocation; Steps({this.startLocation, this.endLocation}); factory Steps.fromJson(Map<String, dynamic> json) { return new Steps( startLocation: new Location( json["start_location"]["lat"], json["start_location"]["lng"]), endLocation: new Location( json["end_location"]["lat"], json["end_location"]["lng"])); } }

Now merge all the code and put google API key.  If you have followed the article carefully, you can see the app running very smoothly as shown in the above video demo. But if you are facing any problem. You can get the working project from Github. 


Source Code               Flutter firebse storage apk

Flutter team has published a google map widget.  Check it here. Flutter - Google map widget plugin

Before run the downloaded project code, make sure. You have replaced google_api_key with your key in Android manifest, map_util.dart, and google_place_util.dart. If still, you have any quires, please feel free to ask it from below comment section.
 

Share:

Get it on Google Play

React Native - Start Development with Typescript

React Native is a popular framework for building mobile apps for both Android and iOS. It allows developers to write JavaScript code that ca...