Maps in Flutter
At AppTree, we recently made the jump to Flutter to replace our existing iOS and Android applications. A major part of our application is mapping.
Flutter is still in alpha and as such, still has functional areas yet to be completely built out. However we find Flutter to be so useful that we prefer to fill any gaps ourselves rather than waiting to adopt it when it’s fully matured. Fortunately, the Flutter team has come up with a great solution by allowing early adopters to build plugins.
Introducing the MapView plugin
As anybody who has used it knows, the GoogleMaps SDK has a large API. We are attempting to tackle the most common use cases first. We started with:
- Markers
- Camera Updates
- Map callbacks
- The ability to show maps inline ( via the Google Maps Static API )
Currently, rendering a native iOS or Android view like a MapView inside of your Flutter hierarchy is a work in progress. Our alternative solution is to use the Google Maps Static API for showing maps inline while using the Google Maps SDK for iOS and Android to allow the user to interact with the maps full screen.
We’ve also built in some convenience methods for generating a static maps given the same viewport and markers you have on the full interactive map.
To demonstrate the use of the plugin, I’ve built a simple app called “NomNom”. The purpose of this application is to find and favorite great restaurants in Portland. You can download the sample project source code here.
Creating your API Key
When using the Google Maps plugin, you first need to create your Google Maps API key. Sign in to https://console.developers.google.com/. If you’ve never used the Google API console before it may ask you to setup a new project.
Once you have your project selected, use the button on the dashboard labeled “Enable APIs and Services”. Depending on the features you use within the plugin, there are 3 APIs to enable
- Android — Enable the ‘Google Maps Android API’
- iOS — Enable the ‘Google Maps iOS API’
- Static Inline Maps — Enable the ‘Google Static Maps API’
For the purposes of this demo, we are also using the ‘Google Places API Web Service’. Enable this one as well.
Next you need to generate an API key. In the drawer on the left hand side, tap on ‘Credentials’. Then tap the ‘Create Credentials’ button and select ‘API Key’.
This will create a new API key that has access to all of the API’s you’ve enabled. It is recommended to restrict the key for production use but for the purposes of this example we’ll leave it as is.
Now that you have the API key, you’ll want to add it to your sample project. At the top of the main.dart
file modify the following line to use your API key:
var apiKey = "<your_api_key>";
You’ll notice our main() function looks like this:
void main() {
var manager = new FavoritesManager();
MapView.setApiKey(apiKey);
runApp(new MyApp(manager));
}
It’s important that the MapView.setApiKey()
method is called before accessing any of the map plugins APIs.
Displaying a full screen interactive map
The first thing that a user will want to do in NomNom is start adding their favorite restaurants. When a user taps the +
button in the lower right hand corner of the screen, we want to display our full screen map. That is handled by the Future _addFavorite()
method inmain.dart.
Future _addFavorite() async {
//1. Show the map
mapView.show(
new MapOptions(
showUserLocation: true,
title: "Choose a favorite",
initialCameraPosition: new CameraPosition(new Location(45.512287, -122.645913), 18.0)),
toolbarActions: <ToolbarAction>[new ToolbarAction("Close", 1)]);
//2. Listen for the onMapReady
var sub = mapView.onMapReady.listen((_) => _updateRestaurantsAroundUser());
compositeSubscription.add(sub);
//3. Listen for camera changed events
sub =
mapView.onCameraChanged.listen((cam) => _updateRestaurantsAroundUser());
compositeSubscription.add(sub);
//4. Listen for toolbar actions
sub = mapView.onToolbarAction.listen((id) {
if (id == 1) {
mapView.dismiss();
}
});
compositeSubscription.add(sub);
}
- First we show the map.
The first argument to MapView.show
is the MapOptions
. Here you can define things like the initial view port, the map type ( Normal, Satellite, Hybrid ) and whether to show the users location.
The show
method also takes an optional list of ToolbarAction
. To keep the full screen map as flexible as possible, we allow you to pass in your own toolbar items. Each toolbar item has a title and identifier.
2. Next we’ll listen for onMapReady
. This method is called once the map is initialized. In our example, we want to find restaurants in the visible viewport of the map so we call _updateRestaurantsAroundUser()
. More on that method later.
3. In Nom Nom, as the user moves the map, we want to update the markers on the map to show the restaurants in the new viewport. We can call the same _updateRestaurantsAroundUser()
method to do that.
4. Lastly, we want to listen for any toolbar actions. Using onToolbarAction
we can listen for any toolbar button presses. This stream sends the identifier
that you provided when you created the ToolbarItem
in step 1. In this case we only have one toolbar item that closes the map view if the user wants to cancel out of adding a favorite.
Finding restaurants within the MapView
As users move the map around we want to show pins on the map for the restaurants in that area. To find the restaurants we are going to use the Google Places API. This API is not part of the MapView plugin but luckily a quick search on pub.dartlang.org produced this plugin so we can easily add this functionality to our app.
As mentioned earlier, the onCameraChanged
callback calls _updateRestaurantsAroundUser
. Let’s break down this method:
Future _updateRestaurantsAroundUser() async {
//1. Ask the mapView for the center lat,lng of it's viewport.
var mapCenter = await mapView.centerLocation;
//2. Search for restaurants using the Places API
var placeApi = new places.GoogleMapsPlaces(apiKey);
var placeResponse = await placeApi.searchNearbyWithRadius(
new places.Location(mapCenter.latitude, mapCenter.longitude), 200,
type: "restaurant");
if (placeResponse.hasNoResults) {
print("No results");
return;
}
var results = placeResponse.results;
//3. Call our _updateMarkersFromResults method update the pins on the map
_updateMarkersFromResults(results);
//4. Listen for the onInfoWindowTapped callback so we know when the user picked a favorite.
var sub = mapView.onInfoWindowTapped.listen((m) {
var selectedResult = results.firstWhere((r) => r.id == m.id);
if (selectedResult != null) {
_addPlaceToFavorites(selectedResult);
}
});
compositeSubscription.add(sub);
}
- First we ask the
mapView
for it’s current center location. This is the center point we will use to search fo restaurants. - Next we use the Places plugin to request nearby restaurants.
- Now that we have the
results
we’ll want to update the markers on the map. To do this we call_updateMarkersFromResults(results);
. More on this method later. - Lastly, when the user taps on the name of the restaurant, we want to add it as a favorite. To be notified of this tap, we can use the
onInfoWindowTapped
stream. The item emitted on this stream is theMarker
that the user tapped on. Now that we know what marker they’ve selected, we can add that place to their favorites list by calling_addPlaceToFavorites.
Adding Markers to the Map
In our last step, after finding restaurants, we called _updateMarkersFromResults
to place pins on the map. Lets explore this method.
The MapView plugin contains the following methods
- MapView.setMarkers
- MapView.addMarker
- MapView.removeMarker
We could use the setMarkers
but that will remove all current markers and then set them again. The disadvantage to this approach is that you are clearing all marker states. This means that if you currently have a marker selected, that selection will be cleared when removed. Instead we want to only add new places and remove places that are no longer visible on the map. For that, we are going to use the addMarker
and removeMarker
methods.
void _updateMarkersFromResults(List<places.PlacesSearchResult> results) {
//1. Turn the list of `PlacesSearchResult` into `Markers`
var markers = results
.map((r) => new Marker(
r.id, r.name, r.geometry.location.lat, r.geometry.location.lng))
.toList();
//2. Get the list of current markers
var currentMarkers = mapView.markers;
//3. Create a list of markers to remove
var markersToRemove = currentMarkers.where((m) => !markers.contains(m));
//4. Create a list of new markers to add
var markersToAdd = markers.where((m) => !currentMarkers.contains(m));
//5. Remove the relevant markers from the map
markersToRemove.forEach((m) => mapView.removeMarker(m));
markersToAdd.forEach((m) => mapView.addMarker(m));
}
- First we need to turn the list of PlaceSearchResults we got back from the Places API into a list of
Marker
. TheMarker
class takes an identifier, name, and lat/lng. Optionally, you can also provide a pin color. - Get the current markers that were previously added to the map. This is going to be the list we compare against to determine whether a marker needs to be added or removed
- Create a list of markers to remove by finding any marker that is no longer contained in our updated
results
. - Create a list of markers to add by finding any marker in
results
that does not currently exist in thecurrentMarkers
. - For each marker in the remove and add list, call the
removeMarker
oraddMarker
.
Conclusion
We hope that with this plugin you’ll be able to start using maps in your Flutter applications. At AppTree, we will continue to evolve this plugin to try to cover the expansive Google Maps API. If you are interested in filing feature requests or contributing you can find the source here. Our team also tries to stay active on the Flutter Gitter channel so we will also try to respond to questions there as we see them.