Adding Google Maps to a Flutter app

Flutter is Google's mobile app SDK for crafting high-quality native experiences on iOS and Android in record time.

With the Google Maps Flutter plugin, you can add maps based on Google maps data to your application. The plugin automatically handles access to the Google Maps servers, map display, and response to user gestures such as clicks and drags. You can also add markers to your map. These objects provide additional information for map locations, and allow the user to interact with the map.

What you'll build

In this codelab, you'll build a mobile app featuring a Google Map using the Flutter SDK. Your app will:

  • Display a Google Map
  • Retrieve map data from a web service
  • Display this data as markers on the Map

What is Flutter?

Flutter is:

  • Fast to develop: Build your Android and iOS applications in milliseconds with Stateful Hot Reload.
  • Expressive and flexible: Quickly ship features with a focus on native end-user experiences.
  • Native performance on both iOS and Android: Flutter's widgets incorporate all critical platform differences — such as scrolling, navigation, icons, and fonts — to provide full native performance.

Google Maps has:

  • 99% coverage of the world: Build with reliable, comprehensive data for over 200 countries and territories.
  • 25 million updates daily: Count on accurate, real-time location information.
  • 1 billion monthly active users: Scale confidently, backed by Google Maps' infrastructure.

This codelab walks you through creating a Google Maps experience in a Flutter app for both iOS and Android.

What you'll learn

  • How to create a new Flutter application.
  • How to configure a Google Maps Flutter plugin.
  • How to add Markers to a map, using location data from a web service.

This codelab focuses on adding a Google map to a Flutter app. Non-relevant concepts and code blocks are glossed over and are provided for you to simply copy and paste.

What would you like to learn from this codelab?

I'm new to the topic, and I want a good overview. I know something about this topic, but I want a refresher. I'm looking for example code to use in my project. I'm looking for an explanation of something specific.

You need two pieces of software to complete this lab: the Flutter SDK, and an editor. This codelab assumes Android Studio, but you can use your preferred editor.

You can run this codelab using any of the following devices:

  • A physical device (Android or iOS) connected to your computer and set to developer mode.
  • The iOS simulator. (Requires installing Xcode tools.)
  • The Android emulator. (Requires setup in Android Studio.)

Getting started with Flutter

The easiest way to get started with Flutter is to use the flutter command line tool to create all the required code for a simple getting started experience.

$ flutter create google_maps_in_flutter
Creating project google_maps_in_flutter...
[Listing of created files elided]
Running "flutter pub get" in google_maps_in_flutter...              2.2s
Wrote 71 files.

All done!
[✓] Flutter: is fully installed. (Channel stable, 1.22.2, on Mac OS X 10.15.7 19H2, locale en)
[✓] Android toolchain - develop for Android devices: is fully installed. (Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS: is fully installed. (Xcode 12.1)
[✓] Android Studio: is fully installed. (version 4.0)
[✓] VS Code: is fully installed. (version 1.50.1)
[✓] Connected device: is fully installed. (2 available)

In order to run your application, type:

  $ cd google_maps_in_flutter
  $ flutter run

Your application code is in google_maps_in_flutter/lib/main.dart.

Adding Google Maps Flutter plugin as a dependency

Adding additional capability to a Flutter app is easy using Pub packages. In this codelab you introduce the Google Maps Flutter plugin by adding a single line to the pubspec.yaml file.

pubspec.yaml

name: google_maps_in_flutter
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  # Add the following line
  google_maps_flutter: ^1.0.3

flutter:
  uses-material-design: true

Download the package with the following command::

$ flutter pub get
Running "flutter pub get" in google_maps_in_flutter...         0.9s 

It's all about the API keys

To use Google Maps in your Flutter app, you need to configure an API project with the Google Maps Platform, following both the Maps SDK for Android's Get API key, and Maps SDK for iOS' Get API key processes. With API keys in hand, carry out the following steps to configure both Android and iOS applications.

Adding an API key for an Android app

To add an API key to the Android app, edit the AndroidManifest.xml file in android/app/src/main. Add a single meta-data entry containing the API key created in the previous step.

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.google_maps_in_flutter">
    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
         calls FlutterMain.startInitialization(this); in its onCreate method.
         In most cases you can leave this as-is, but you if you want to provide
         additional functionality it is fine to subclass or reimplement
         FlutterApplication and put your custom class here. -->
    <application
        android:name="io.flutter.app.FlutterApplication"
        android:label="google_maps_in_flutter"
        android:icon="@mipmap/ic_launcher">

        <!-- TODO: Add your API key here -->
        <meta-data android:name="com.google.android.geo.API_KEY"
               android:value="YOUR-KEY-HERE"/>

        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <!-- Displays an Android View that continues showing the launch screen
                 Drawable until Flutter paints its first frame, then this splash
                 screen fades out. A splash screen is useful to avoid any visual
                 gap between the end of Android's launch screen and the painting of
                 Flutter's first frame. -->
            <meta-data
              android:name="io.flutter.embedding.android.SplashScreenDrawable"
              android:resource="@drawable/launch_background"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

Adding an API key for an iOS app

To add an API key to the iOS app, edit the AppDelegate.swift file in ios/Runner. Unlike Android, adding an API key on iOS requires changes to the source code of the Runner app. The AppDelegate is the core singleton that is part of the app initialization process.

Make two changes to this file. First, add an #import statement to pull in the Google Maps headers, and then call the provideAPIKey() method of the GMSServices singleton. This API key enables Google Maps to correctly serve map tiles.

ios/Runner/AppDelegate.swift

import UIKit
import Flutter
import GoogleMaps

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    // TODO: Add your API key
    GMSServices.provideAPIKey("YOUR-API-KEY")

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Putting a map on the screen

Now it's time to get a map on the screen. Update lib/main.dart as follows:.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

void main() => runApp(MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  GoogleMapController mapController;

  final LatLng _center = const LatLng(45.521563, -122.677433);

  void _onMapCreated(GoogleMapController controller) {
    mapController = controller;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Maps Sample App'),
          backgroundColor: Colors.green[700],
        ),
        body: GoogleMap(
          onMapCreated: _onMapCreated,
          initialCameraPosition: CameraPosition(
            target: _center,
            zoom: 11.0,
          ),
        ),
      ),
    );
  }
}

Running the app

Run the Flutter app in either iOS or Android to see a single map view, centered on Portland. Feel free to modify the map center to represent your hometown, or somewhere that is important to you.

$ flutter run

Google has many offices around the world, from North America, Latin America, Europe, Asia Pacific, to Africa & Middle East. The nice thing about these maps, if you investigate them, is that they have an easily usable API endpoint for supplying the office location information in JSON format. In this step, you put these office locations on the map.

As you grow your codebase, it's time to start using tooling that Dart provides to make the code more readable and maintainable. In this step, you use code generation to parse JSON, and code linting to surface potential code smells.

To use these capabilities, add some new dependencies to the pubspec.yaml file. These dependencies provide access to http requests, the ability to mechanize JSON parsing, a configuration of useful lint rules used widely at Google, and a build runner that ties all of it together. Edit the dependencies stanza of your pubspec.yaml file as follows:

pubspec.yaml

name: google_maps_in_flutter
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=2.7.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  google_maps_flutter: ^0.5.27+3
  # Add the following two lines
  http: ^0.12.0+1
  json_serializable: ^3.3.0

# Add the following three lines
dev_dependencies:
  pedantic: ^1.9.0
  build_runner: ^1.10.0

flutter:
  uses-material-design: true

Run flutter packages get on the command line to retrieve these new dependencies, and to prepare the app for the next stages.

$ flutter pub get 
Running "flutter pub get" in google_maps_in_flutter...         0.5s
$

Using the providing tooling

Two of the nice additions to programming languages in recent years are pragmatic defaults for code formatting, and linting for known problematic code patterns. For code formatting, you can use flutter format, although you can probably configure your code editor to run this on a certain key combination, or on file save.

$ flutter format .
Formatting directory .:
Skipping link ios/.symlinks/plugins/flutter_plugin_android_lifecycle
Skipping link ios/.symlinks/plugins/google_maps_flutter
Formatted lib/main.dart
$

For linting, Dart provides the ability to configure a customized code linter. In this step, you add a handful of linters to the app, but the full list of available lints is specified in the Linter for Dart documentation.

Add a file to the root of the project called analysis_options.yaml and fill it with the following content.

analysis_options.yaml

include: package:pedantic/analysis_options.yaml

analyzer:
  exclude:
    - lib/src/locations.g.dart

linter:
  rules:
    - always_declare_return_types
    - camel_case_types
    - empty_constructor_bodies
    - annotate_overrides
    - avoid_init_to_null
    - constant_identifier_names
    - one_member_abstracts
    - slash_for_doc_comments
    - sort_constructors_first
    - unnecessary_brace_in_string_interps

The first line includes a default set of rules used widely at Google, and the linter rules section gives a taste of what is possible. The exclude line references a file that hasn't been generated yet. To run the lint rules, analyze the code as follows:

$ flutter analyze .
Analyzing google_maps_in_flutter...                                     
No issues found! (ran in 1.8s)
$

If the analyzer issues warnings, don't worry, you are going to fix them shortly.

Parsing JSON with code generation

You might notice that the JSON data returned from the API endpoint has a regular structure. It would be handy to generate the code to marshal that data into objects that you can use in code. While Dart provides a variety of options for de-serializing JSON data (from build-it-yourself, to signing the data and using built_value), this step uses JSON annotations.

In the lib/src directory, create a locations.dart file and describe the structure of the returned JSON data as follows:

lib/src/locations.dart

import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:json_annotation/json_annotation.dart';

part 'locations.g.dart';

@JsonSerializable()
class LatLng {
  LatLng({
    this.lat,
    this.lng,
  });

  factory LatLng.fromJson(Map<String, dynamic> json) => _$LatLngFromJson(json);
  Map<String, dynamic> toJson() => _$LatLngToJson(this);

  final double lat;
  final double lng;
}

@JsonSerializable()
class Region {
  Region({
    this.coords,
    this.id,
    this.name,
    this.zoom,
  });

  factory Region.fromJson(Map<String, dynamic> json) => _$RegionFromJson(json);
  Map<String, dynamic> toJson() => _$RegionToJson(this);

  final LatLng coords;
  final String id;
  final String name;
  final double zoom;
}

@JsonSerializable()
class Office {
  Office({
    this.address,
    this.id,
    this.image,
    this.lat,
    this.lng,
    this.name,
    this.phone,
    this.region,
  });

  factory Office.fromJson(Map<String, dynamic> json) => _$OfficeFromJson(json);
  Map<String, dynamic> toJson() => _$OfficeToJson(this);

  final String address;
  final String id;
  final String image;
  final double lat;
  final double lng;
  final String name;
  final String phone;
  final String region;
}

@JsonSerializable()
class Locations {
  Locations({
    this.offices,
    this.regions,
  });

  factory Locations.fromJson(Map<String, dynamic> json) =>
      _$LocationsFromJson(json);
  Map<String, dynamic> toJson() => _$LocationsToJson(this);

  final List<Office> offices;
  final List<Region> regions;
}

Future<Locations> getGoogleOffices() async {
  const googleLocationsURL = 'https://about.google/static/data/locations.json';

  // Retrieve the locations of Google offices
  final response = await http.get(googleLocationsURL);
  if (response.statusCode == 200) {
    return Locations.fromJson(json.decode(response.body));
  } else {
    throw HttpException(
        'Unexpected status code ${response.statusCode}:'
        ' ${response.reasonPhrase}',
        uri: Uri.parse(googleLocationsURL));
  }
}

Once you've added this code, your IDE (if you are using one) should display some red squiggles, as it references a nonexistent sibling file, locations.g.dart. This generated file converts between untyped JSON structures and named objects. Create it by running the build_runner:

$ flutter pub run build_runner build --delete-conflicting-outputs
[INFO] Generating build script...
[INFO] Generating build script completed, took 291ms

[INFO] Initializing inputs
[INFO] Reading cached asset graph...
[INFO] Reading cached asset graph completed, took 65ms

[INFO] Checking for updates since last build...
[INFO] Checking for updates since last build completed, took 595ms

[INFO] Running build...
[INFO] 1.2s elapsed, 0/1 actions completed.
[INFO] Running build completed, took 1.2s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 27ms

[INFO] Succeeded after 1.2s with 1 outputs (1 actions)

$

Your code should now analyze cleanly again.

Modify the main.dart file to request the map data, and then use the returned info to add offices to the map:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'src/locations.dart' as locations;

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final Map<String, Marker> _markers = {};
  Future<void> _onMapCreated(GoogleMapController controller) async {
    final googleOffices = await locations.getGoogleOffices();
    setState(() {
      _markers.clear();
      for (final office in googleOffices.offices) {
        final marker = Marker(
          markerId: MarkerId(office.name),
          position: LatLng(office.lat, office.lng),
          infoWindow: InfoWindow(
            title: office.name,
            snippet: office.address,
          ),
        );
        _markers[office.name] = marker;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Google Office Locations'),
          backgroundColor: Colors.green[700],
        ),
        body: GoogleMap(
          onMapCreated: _onMapCreated,
          initialCameraPosition: CameraPosition(
            target: const LatLng(0, 0),
            zoom: 2,
          ),
          markers: _markers.values.toSet(),
        ),
      ),
    );
  }
}

This code performs several operations:

  • In _onMapCreated, it uses the JSON parsing code from the previous step, awaiting until it's loaded. It then uses the returned data to create Markers inside a setState() callback. Once the app receives new markers, setState flags Flutter to repaint the screen, causing the office locations to display.
  • The markers are stored in a Map that is associated with the GoogleMap widget. This links the markers to the correct map. You could, of course, have multiple maps and display different markers in each.

ce32c492a689e181.png

Here's a screenshot of what you have accomplished. There are many interesting additions that can be made at this point. For example, you could add a list view of the offices that moves and zooms the map when the user clicks an office but, as they say, this exercise is left to the reader!

Congratulations!

You have completed the codelab and have built a Flutter app with a Google Map! You've also interacted with a JSON Web Service.

Other next steps

This codelab has built an experience to visualise a number of points on a map. There are a number of mobile apps that build on this capability to serve a lot of different user needs. There are other resources that can help you take this further: