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:

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);
}
  1. 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 ToolbarItemin 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);
}
  1. First we ask the mapView for it’s current center location. This is the center point we will use to search fo restaurants.
  2. Next we use the Places plugin to request nearby restaurants.
  3. 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.
  4. 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 the Marker 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));
}
  1. First we need to turn the list of PlaceSearchResults we got back from the Places API into a list of Marker . The Marker class takes an identifier, name, and lat/lng. Optionally, you can also provide a pin color.
  2. 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
  3. Create a list of markers to remove by finding any marker that is no longer contained in our updated results.
  4. Create a list of markers to add by finding any marker in results that does not currently exist in the currentMarkers.
  5. For each marker in the remove and add list, call the removeMarker or addMarker.

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.