1. はじめに
Flutter は Google のモバイルアプリ SDK で、iOS および Android 向けの高品質なネイティブ エクスペリエンスを迅速に構築できるのが特長です。
Google マップの Flutter プラグインを使用すれば、Google マップのデータに基づく地図をアプリケーションに追加することができます。このプラグインは、Google マップのサーバーへのアクセス、地図の表示、クリックやドラッグといったユーザーのジェスチャーへの反応を、自動的に処理します。また、地図にマーカーを追加することも可能です。これらのオブジェクトは地図上の場所に関する追加情報となり、ユーザーはこれらを通じて地図を操作できます。
作成する内容
| この Codelab では Flutter SDK を使って、Google マップを組み込んだモバイルアプリを作成します。アプリには次の機能を持たせます。 
 | 
 | 
Flutter とは
Flutter には 3 つの主な特長があります。
- 迅速な開発: ステートフルなホットリロードにより、Android / iOS アプリケーションをごく短時間で構築できます。
- 表現力と柔軟性: ネイティブなエンドユーザー エクスペリエンスを重視し、機能をすばやくリリースできます。
- iOS と Android の両方に対応するネイティブ パフォーマンス: プラットフォーム間の重要な違い(スクロール、ナビゲーション、アイコン、フォントなど)はすべて Flutter のウィジェットに組み込まれており、存分にネイティブ パフォーマンスを発揮できます。
Google マップの特長:
- 世界の 99% をカバー: 200 を超える国や地域の信頼できる広範なデータを利用できます。
- 1 日あたり 2,500 万件のアップデート: 正確でリアルタイム性の高いロケーション情報が供給されます。
- 月間アクティブ ユーザー数 10 億人: Google マップのインフラを基盤に、安心してスケールアップできます。
この Codelab では、iOS および Android 用の Flutter アプリ内に Google マップのエクスペリエンスを作成する手順を解説します。
学習する内容
- 新しい Flutter アプリケーションを作成する方法
- Google マップの Flutter プラグインの設定方法
- ウェブサービスのロケーション データを使って地図にマーカーを追加する方法
この Codelab では、Flutter アプリに Google マップを追加することに重点を置きます。関連のない概念やコードブロックについては詳しく触れず、コピーして貼り付ければ済む形で提供します。
この Codelab で学びたいことは次のどれですか?
2. Flutter 環境をセットアップする
このラボを完了するには、Flutter SDK とエディタの 2 つのソフトウェアが必要です。説明は Android Studio を前提としたものになっていますが、実際に使用するのはどのエディタでもかまいません。
この Codelab は、次のデバイスのどれを使用しても実行できます。
- Android または iOS デバイスの実機(パソコンに接続し、デベロッパー モードに設定したもの)
- iOS シミュレータ(Xcode ツールのインストールが必要)
- Android Emulator(Android Studio でのセットアップが必要)
3. 始める
Flutter の使用を開始する
Flutter の使用を開始する最も手軽な方法は、基本的な開発に必要なコード一式を Flutter コマンドライン ツールで作成することです。
$ flutter create google_maps_in_flutter --platforms android,ios,web Creating project google_maps_in_flutter... Running "flutter pub get" in google_maps_in_flutter... 1,612ms Wrote 80 files. All done! 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.
Google マップの Flutter プラグインを依存関係として追加する
Flutter アプリに機能を追加するには、Pub パッケージを使用すると簡単です。この Codelab では、Google マップの Flutter プラグインを導入するため、次のコマンドをプロジェクト ディレクトリから実行します。
$ cd google_maps_in_flutter $ flutter pub add google_maps_flutter Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) + flutter_plugin_android_lifecycle 2.0.6 + google_maps_flutter 2.1.8 + google_maps_flutter_platform_interface 2.2.0 matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) + plugin_platform_interface 2.1.2 source_span 1.8.2 (1.9.0 available) + stream_transform 2.0.0 string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) Changed 5 dependencies!
この Codelab では、Flutter for Web で Google マップを使用する方法も説明します。プラグインのウェブ版はまだ連携化されていないため、こちらも別個にプロジェクトに追加する必要があります。
$ flutter pub add google_maps_flutter_web Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) + csslib 0.17.2 fake_async 1.3.0 (1.3.1 available) + flutter_web_plugins 0.0.0 from sdk flutter + google_maps 6.2.0 + google_maps_flutter_web 0.4.0+1 + html 0.15.0 + js 0.6.4 + js_wrapping 0.7.4 matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) + sanitize_html 2.0.0 source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) Downloading google_maps_flutter_web 0.4.0+1... Changed 8 dependencies!
iOS の platform の設定
iOS で Google Maps SDK の最新バージョンを使用するには、プラットフォームの最低バージョンを iOS 13 にする必要があります。ios/Podfile を次のように変更しましょう。
ios/Podfile
# Set platform to 13.0 to enable latest Google Maps SDK
platform :ios, '13.0' # Uncomment and set to 13.
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Android の minSDK の設定
Android で Google Maps SDK を使用するには、minSDK を 20 に設定する必要があります。android/app/build.gradle を次のように変更しましょう。
android/app/build.gradle
android {
    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.google_maps_in_flutter"
        minSdkVersion 20                      // Set to 20
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
}
4. アプリに Google マップを追加する
重要なのは API キー
Flutter アプリ内で Google マップを使用するには、Google Maps Platform で API プロジェクトを設定する必要があります。各プラットフォームの手順(Maps SDK for Android / Maps SDK for iOS / Maps JavaScript API)に従いましょう。API キーを入手したら、次の手順に従って、Android アプリと iOS アプリの両方の設定を行います。
Android アプリに API キーを追加する
Android アプリに API キーを追加するには、android/app/src/main の AndroidManifest.xml ファイルを編集します。application ノード内に単一の meta-data エントリを追加し、ここまでの手順で作成した API キーを指定しましょう。
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.google_maps_in_flutter">
    <application
        android:label="google_maps_in_flutter"
        android:icon="@mipmap/ic_launcher">
        <!-- TODO: Add your Google Maps 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">
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <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>
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>
iOS アプリに API キーを追加する
iOS アプリに API キーを追加するには、ios/Runner の AppDelegate.swift ファイルを編集します。Android と異なり、iOS で API キーを追加するには、Runner アプリのソースコードを変更する必要があります。AppDelegate はアプリ初期化プロセスの一部となるコア シングルトンです。
このファイルに 2 か所変更を加えます。まず Google マップのヘッダーを取り込むために #import ステートメントを追加し、次に GMSServices シングルトンの provideAPIKey() メソッドを呼び出します。この API キーにより、Google マップが地図タイルを正しく配信できるようになります。
ios/Runner/AppDelegate.swift
import UIKit
import Flutter
import GoogleMaps  // Add this import
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    // TODO: Add your Google Maps API key
    GMSServices.provideAPIKey("YOUR-API-KEY")
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
ウェブアプリに API キーを追加する
ウェブアプリに API キーを追加するには、web の index.html ファイルを編集します。head セクションに Google マップの JavaScript スクリプトへの参照を追加し、API キーを指定しましょう。
web/index.html
<head>
  <base href="/">
  <meta charset="UTF-8">
  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
  <meta name="description" content="A new Flutter project.">
  <!-- iOS meta tags & icons -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="google_maps_in_flutter">
  <link rel="apple-touch-icon" href="icons/Icon-192.png">
  <!-- TODO: Add your Google Maps API key here -->
  <script src="https://maps.googleapis.com/maps/api/js?key=YOUR-KEY-HERE"></script>
  <title>google_maps_in_flutter</title>
  <link rel="manifest" href="manifest.json">
</head>
画面に地図を配置する
では、実際に画面に地図を表示してみましょう。lib/main.dart を次のように更新します。
lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
  late GoogleMapController mapController;
  final LatLng _center = const LatLng(45.521563, -122.677433);
  void _onMapCreated(GoogleMapController controller) {
    mapController = controller;
  }
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.green[700],
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Maps Sample App'),
          elevation: 2,
        ),
        body: GoogleMap(
          onMapCreated: _onMapCreated,
          initialCameraPosition: CameraPosition(
            target: _center,
            zoom: 11.0,
          ),
        ),
      ),
    );
  }
}
アプリを実行する
iOS または Android で Flutter アプリを実行すると、ポートランド市が中心にある単一の地図ビューが表示されます。起動する環境は Android Emulator や iOS シミュレータでもかまいません。地図の中心位置は自由に変更してみましょう(故郷の街、大切な場所など)。
$ flutter run
| 
 | 
 | 
5. Google のオフィスを地図に表示する
Google のオフィスは、北米、中南米、ヨーロッパ、アジア太平洋、アフリカおよび中東と、世界中に多数点在しています。これらの地図を調べてみると、実は手軽に使える API エンドポイントが用意されており、オフィスの位置の情報を JSON 形式で入手することができます。このステップでは、これらのオフィスの位置を地図上に表示します。JSON データのパースには、コード生成を使用します。
次のように、新たに 3 つの Flutter 依存関係をプロジェクトに追加します。まず、HTTP リクエストを簡単に行うための http パッケージを追加します。
$ flutter pub add http Resolving dependencies... async 2.8.1 (2.8.2 available) + http 0.13.3 + http_parser 4.0.0 matcher 0.12.10 (0.12.11 available) + pedantic 1.11.1 test_api 0.4.2 (0.4.3 available) Changed 3 dependencies!
次に、JSON ドキュメントを表すオブジェクト構造を宣言するための json_serializable と json_annotation を追加します。
$ flutter pub add json_serializable Resolving dependencies... + _fe_analyzer_shared 25.0.0 + analyzer 2.2.0 + args 2.2.0 async 2.8.1 (2.8.2 available) + build 2.1.0 + build_config 1.0.0 + checked_yaml 2.0.1 + cli_util 0.3.3 + convert 3.0.1 + crypto 3.0.1 + dart_style 2.0.3 + file 6.1.2 + glob 2.0.1 + json_annotation 4.1.0 + json_serializable 5.0.0 + logging 1.0.1 matcher 0.12.10 (0.12.11 available) + package_config 2.0.0 + pub_semver 2.0.0 + pubspec_parse 1.0.0 + source_gen 1.1.0 + source_helper 1.2.1 test_api 0.4.2 (0.4.3 available) + watcher 1.0.0 + yaml 3.1.0 Downloading analyzer 2.2.0... Downloading _fe_analyzer_shared 25.0.0... Changed 22 dependencies! $ flutter pub add json_annotation Resolving dependencies... async 2.8.2 (2.9.0 available) characters 1.2.0 (1.2.1 available) clock 1.1.0 (1.1.1 available) fake_async 1.3.0 (1.3.1 available) matcher 0.12.11 (0.12.12 available) material_color_utilities 0.1.4 (0.1.5 available) meta 1.7.0 (1.8.0 available) path 1.8.1 (1.8.2 available) source_span 1.8.2 (1.9.0 available) string_scanner 1.1.0 (1.1.1 available) term_glyph 1.2.0 (1.2.1 available) test_api 0.4.9 (0.4.12 available) Got dependencies!
最後に、開発時用の依存関係として build_runner を追加します。これは後述のコード生成に使用します。
$ flutter pub add --dev build_runner Resolving dependencies... async 2.8.1 (2.8.2 available) + build_daemon 3.0.0 + build_resolvers 2.0.4 + build_runner 2.1.1 + build_runner_core 7.1.0 + built_collection 5.1.0 + built_value 8.1.2 + code_builder 4.1.0 + fixnum 1.0.0 + frontend_server_client 2.1.2 + graphs 2.0.0 + http_multi_server 3.0.1 + io 1.0.3 + js 0.6.3 matcher 0.12.10 (0.12.11 available) + mime 1.0.0 + pool 1.5.0 + shelf 1.2.0 + shelf_web_socket 1.0.1 test_api 0.4.2 (0.4.3 available) + timing 1.0.0 + web_socket_channel 2.1.0 Changed 19 dependencies!
コード生成によって JSON データをパースする
API エンドポイントから返される JSON データを調べると、決まった構造があることがわかります。この形式のデータをコード内で使用可能なオブジェクトに整形するコードを生成できれば便利なはずです。
lib/src ディレクトリに locations.dart ファイルを作成し、エンドポイントから返される JSON データの構造を次のように記述しましょう。
lib/src/locations.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:json_annotation/json_annotation.dart';
part 'locations.g.dart';
@JsonSerializable()
class LatLng {
  LatLng({
    required this.lat,
    required 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({
    required this.coords,
    required this.id,
    required this.name,
    required 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({
    required this.address,
    required this.id,
    required this.image,
    required this.lat,
    required this.lng,
    required this.name,
    required this.phone,
    required 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({
    required this.offices,
    required 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
  try {
    final response = await http.get(Uri.parse(googleLocationsURL));
    if (response.statusCode == 200) {
      return Locations.fromJson(
          json.decode(response.body) as Map<String, dynamic>);
    }
  } catch (e) {
    if (kDebugMode) {
      print(e);
    }
  }
  // Fallback for when the above HTTP request fails.
  return Locations.fromJson(
    json.decode(
      await rootBundle.loadString('assets/locations.json'),
    ) as Map<String, dynamic>,
  );
}
このコードを追加すると、IDE を使用している場合は、赤い波線がいくつか表示されるはずです。これは、存在しないシブリング ファイル locations.g.dart. を参照しているためです。生成されたこのファイルは、型なしの JSON 構造と名前付きオブジェクトの間の変換を行います。build_runner を実行してファイルを作成してみましょう。
$ flutter pub run build_runner build --delete-conflicting-outputs [INFO] Generating build script... [INFO] Generating build script completed, took 357ms [INFO] Creating build script snapshot...... [INFO] Creating build script snapshot... completed, took 10.5s [INFO] There was output on stdout while compiling the build script snapshot, run with `--verbose` to see it (you will need to run a `clean` first to re-snapshot). [INFO] Initializing inputs [INFO] Building new asset graph... [INFO] Building new asset graph completed, took 646ms [INFO] Checking for unexpected pre-existing outputs.... [INFO] Deleting 1 declared outputs which already existed on disk. [INFO] Checking for unexpected pre-existing outputs. completed, took 3ms [INFO] Running build... [INFO] Generating SDK summary... [INFO] 3.4s elapsed, 0/3 actions completed. [INFO] Generating SDK summary completed, took 3.4s [INFO] 4.7s elapsed, 2/3 actions completed. [INFO] Running build completed, took 4.7s [INFO] Caching finalized dependency graph... [INFO] Caching finalized dependency graph completed, took 36ms [INFO] Succeeded after 4.8s with 2 outputs (7 actions)
IDE のコード分析が再度きれいになっているはずです。次に、getGoogleOffices 関数でフォールバックとして使用する locations.json ファイルを追加します。このフォールバックを組み込む理由としては、この関数で読み込まれる静的データは CORS ヘッダーなしで配信されるため、ウェブブラウザでの読み込みに失敗することが挙げられます。Android / iOS の Flutter アプリは CORS ヘッダーを必要としないのですが、モバイル データ アクセスはとかく制約が多いものです。
ブラウザで https://about.google/static/data/locations.json を開き、内容をアセット ディレクトリに保存します。または、コマンドラインで以下を行います。
$ mkdir assets
$ cd assets
$ curl -o locations.json https://about.google/static/data/locations.json
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 30348  100 30348    0     0  75492      0 --:--:-- --:--:-- --:--:-- 75492
これでアセット ファイルをダウンロードできたので、pubspec.yaml ファイルの flutter セクションに追加しましょう。
pubspec.yaml
flutter:
  uses-material-design: true
  assets:
    - assets/locations.json
main.dart ファイルを変更して地図データをリクエストし、返された情報を使って各オフィスを地図に追加します。
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(const MyApp());
}
class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  State<MyApp> 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(
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.green[700],
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Google Office Locations'),
          elevation: 2,
        ),
        body: GoogleMap(
          onMapCreated: _onMapCreated,
          initialCameraPosition: const CameraPosition(
            target: LatLng(0, 0),
            zoom: 2,
          ),
          markers: _markers.values.toSet(),
        ),
      ),
    );
  }
}
このコードは次のような処理を行います。
- _onMapCreatedでは前述の JSON パース処理コードを使用しており、同コードの読み込みを- awaitするようになっています。その後、返されたデータを使って- setState()コールバック内に- Markerを作成しています。アプリが新しいマーカーを受け取ると、setState が Flutter に画面をリペイントするよう指示し、各オフィスの位置が表示されます。
- マーカーは、GoogleMapウィジェットと関連付けられたMap内に格納されます。これにより、マーカーが適切な地図とリンクされます。もちろん、複数の地図にそれぞれ異なるマーカーを表示することもできます。

ここまでの成果のスクリーンショットです。この時点でさまざまな興味深い追加機能を組み込むことも可能です。たとえばオフィスの一覧を表示して、ユーザーがオフィスを選択すればそれに合わせて地図が動いたりズームしたりする、といった機能が考えられますが、これはいわゆる「読者への宿題」の領分になるでしょう。よろしければお試しください。
6 次のステップ
おつかれさまでした
この Codelab はこれで完了です。Google マップを組み込んだ Flutter アプリが完成し、JSON ウェブサービスとやりとりする方法も確認できました。
次のステップ(応用)
この Codelab では、地図上で複数の地点を視覚化するエクスペリエンスを構築しました。これは幅広いユーザーニーズに対応できる機能で、さまざまなモバイルアプリに活用されています。さらなる応用に向けて、次のようなリソースをおすすめします。
- Build Mobile Apps With Flutter and Google Maps(Cloud Next ‘19 での講演)
- Hadrien Lejard 氏の google_maps_webserviceパッケージ: Google マップのウェブサービス(Directions API、Distance Matrix API、Places API など)をとても手軽に利用できるパッケージです。
- JSON REST を介した API 利用のさまざまな方法に関心がある場合は、Andrew Brogdon が Medium に公開している記事がおすすめです。

