Flutter has been gaining popularity since it made its debut. In this tutorial, we are going to leverage the power of Maps SDK and Google Maps Web Services and build a map that shows nearby restaurants to the user. To achieve that, we need to add three external packages to our project:
Google Maps for Flutter - To add a Google Map to our application.
Flutter Geolocator Plugin - To get the location of the user.
Google Maps Web Services - To access Google Maps Web Services.
Important*: Please note that you may need to mock locations when using a simulator/emulator throughout this tutorial.
Prerequisites: Flutter SDK and an editor.
Once you have created a Flutter project, open pubspec.yaml
located on the root of your project and add the above-mentioned dependencies to your Flutter project. Under dependencies:
, add google_maps_flutter: ^0.5.25+1
, geolocator: ^5.3.0
and google_maps_webservice: ^0.0.16
. Remember to save the changes.
In order to add Google Maps to the project, you will need to complete the following procedures:
Configure a project with the Google Maps Platform.
In the console page, bring up the navigation menu (represented by a hamburger icon), click “APIs & Services”, then click “Credentials”.
Click “Create Credentials” and choose “API key”.
You are encouraged to restrict your keys. If you wish to run your project on both the Android and iOS platforms, you will need to create two separate keys and restrict their usage to the platform accordingly.
Add the API keys for the Android App and/or iOS App.
Please refer to the documentation here (under the section ”Android” and ”iOS”).
… and we can finally start writing some codes!
We’ll start with something really simple: showing the Google Map. In addition, on the rendered map, we’ll mark the location of the Googleplex by using a Marker object. Replace the code in lib/main.dart
with the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Foodie Map",
home: Scaffold(
// We'll change the AppBar title later
appBar: AppBar(title: Text("Googleplex's Location")),
body: FoodieMap()),
);
}
}
class FoodieMap extends StatefulWidget {
@override
State < StatefulWidget > createState() {
return _FoodieMapState();
}
}
class _FoodieMapState extends State < FoodieMap > {
// Location of the Googleplex
final _googlePlex = LatLng(37.4220, -122.0840);
// Set of markers to be drawn on the map
Set < Marker > _markers = {};
@override
Widget build(BuildContext context) {
return GoogleMap(
initialCameraPosition: CameraPosition(
target: _googlePlex,
zoom: 12.0,
),
// Cascades notation that adds the Googleplex Marker in the markers set
// and returns the reference to the set
markers: _markers..add(Marker(
markerId: MarkerId("Google Plex"),
infoWindow: InfoWindow(title: "Google Plex"),
position: _googlePlex)),
);
}
}
Run the code and you should be able to see an output similar to this:
https://photos.app.goo.gl/416jpHp3cVCxSkxB8
Flutter is all about widgets. According to the documentation, a widget’s build
method is responsible for constructing the user interface represented by itself. To create a holistic user interface, widgets are commonly composited together. In our application, the Material App widget contains a Scaffold widget (which embeds some useful material design layouts like an AppBar), the Scaffold widget then contains our custom widget, the FoodieMap.
A widget can be either Stateless or Stateful. Their fundamental difference is a Stateless widget’s build method does not depend on anything other than the information known to its own class (in fact, the FoodieMap and its State could be refactored into a single Stateless widget for now). While a Stateless widget implements the build
method itself, a Stateful widget delegates such responsibility to its State class. The Stateful widget class itself merely needs to implement a createState
method that returns a State object.
Next, we will modify the program to display the location of the user instead of the Googleplex. That means the build
method of the class will now depend on the user location, which is unknown to us at the compiling time.
To get the current location of the user, we will need to use the Geolocator. For security reasons, we will need to do some additional setups to seek permissions on the platform(s).
Follow the steps specified here, under the “Permission” section.
After that, we are going to get the user location by calling Geolocator().getCurrentPosition()
and replace the hardcoded Googleplex location with the value returned. The mentioned function call is asynchronous, hence we need to use a Future Builder here, which is ideal for building widgets when Future data is involved. The future object must exist before the build
method call, therefore we will create the object in the initState
method.
First, import the package:
Then modify the _FoodieMapState
class as below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class _FoodieMapState extends State < FoodieMap > {
Future < Position > _currentLocation;
Set < Marker > _markers = {};
@override
void initState() {
super.initState();
_currentLocation = Geolocator()
.getCurrentPosition();
}
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _currentLocation,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
// The user location returned from the snapshot
Position snapshotData = snapshot.data;
LatLng _userLocation =
LatLng(snapshotData.latitude, snapshotData.longitude);
return GoogleMap(
initialCameraPosition: CameraPosition(
target: _userLocation,
zoom: 12,
),
markers: _markers..add(Marker(
markerId: MarkerId("User Location"),
infoWindow: InfoWindow(title: "User Location"),
position: _userLocation)),
);
} else {
return Center(child: Text("Failed to get user location."));
}
}
// While the connection is not in the done state yet
return Center(child: CircularProgressIndicator());
});
}
}
Run the code and you should be able to get a similar output:
https://photos.app.goo.gl/R392fVhoVg6vdn2R6
Grant the app permission and you will be able to see your current location on the map!
Now, we’re finally integrating the last external package, the Google Maps Web Service. To be exact, we are only using the Places API. In the Google Maps Platform console page, create a new API key and restrict it to call only the Places API (you may need to enable the API first if it does not show up in the restriction settings).
First, import the package:
Then, add a single line below the main
method to instantiate an API client:final places = GoogleMapsPlaces(apiKey: "YOUR-API-KEY");
In the build
method of the _FoodieMapState
, after the declaration of the _userLocation
, add the following codes to retrieve the nearby restaurants:
1
2
3
if (_markers.isEmpty) {
_retrieveNearbyRestaurants(_userLocation);
}
Now, we will implement the _retrieveNearbyRestaurants
function. It should call the setState
method to notify Flutter that the _markers
set has been updated with the restaurant markers to schedule a build
for the State.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Future < void > _retrieveNearbyRestaurants(LatLng _userLocation) async {
PlacesSearchResponse _response = await places.searchNearbyWithRadius(
Location(_userLocation.latitude, _userLocation.longitude), 10000,
type: "restaurant");
Set < Marker > _restaurantMarkers = _response.results
.map((result) => Marker(
markerId: MarkerId(result.name),
// Use an icon with different colors to differentiate between current location
// and the restaurants
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueAzure),
infoWindow: InfoWindow(
title: result.name,
snippet: "Ratings: " + (result.rating?.toString() ?? "Not Rated")),
position: LatLng(
result.geometry.location.lat, result.geometry.location.lng)))
.toSet();
setState(() {
_markers.addAll(_restaurantMarkers);
});
}
Using the API client that we have instantiated earlier, we call its searchNearbyWithRadius
method and pass in the location to start the search with. We also pass along some optional parameters including the radius in meter to search for and the restrictions of the type of the results. Feel free to play around with the supported types here.
After the mapping process, we will get a set of new restaurant markers and we will wrap the updated _markers
to be drawn on the map inside the setState
callback to notify Flutter of this change and schedule a build
for it.
Recall that the invocation to this method is wrapped by an if statement to prevent an infinite loop between build
and _retrieveNearbyRestaurants
.
Run the code and … tadaa! Time to satisfy your cravings!
https://photos.app.goo.gl/mawBtcBU9Q3QNYmy5