Flutter アプリに Google マップを追加する

1. はじめに

Flutter は Google のモバイルアプリ SDK で、iOS および Android 向けの高品質なネイティブ エクスペリエンスを迅速に構築できるのが特長です。

Google マップの Flutter プラグインを使用すれば、Google マップのデータに基づく地図をアプリケーションに追加することができます。このプラグインは、Google マップのサーバーへのアクセス、地図の表示、クリックやドラッグといったユーザーのジェスチャーへの反応を、自動的に処理します。また、地図にマーカーを追加することも可能です。これらのオブジェクトは地図上の場所に関する追加情報となり、ユーザーはこれらを通じて地図を操作できます。

作成する内容

この Codelab では Flutter SDK を使って、Google マップを組み込んだモバイルアプリを作成します。アプリには次の機能を持たせます。

  • 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
Creating project google_maps_in_flutter...
[Listing of created files elided]
Wrote 127 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.6.1 (2.8.2 available)
  charcode 1.2.0 (1.3.1 available)
+ flutter_plugin_android_lifecycle 2.0.3
+ google_maps_flutter 2.0.8
+ google_maps_flutter_platform_interface 2.1.1
  matcher 0.12.10 (0.12.11 available)
  meta 1.3.0 (1.7.0 available)
+ plugin_platform_interface 2.0.1
+ stream_transform 2.0.0
  test_api 0.3.0 (0.4.3 available)
Downloading google_maps_flutter 2.0.8...
Downloading flutter_plugin_android_lifecycle 2.0.3...
Changed 5 dependencies!

この Codelab では、Flutter for Web で Google マップを使用する方法も説明します。プラグインのウェブ版はまだ連携化されていないため、こちらも別個にプロジェクトに追加する必要があります。

$ flutter pub add google_maps_flutter_web
Resolving dependencies...
  async 2.6.1 (2.8.2 available)
  charcode 1.2.0 (1.3.1 available)
+ csslib 0.17.0
+ flutter_web_plugins 0.0.0 from sdk flutter
+ google_maps 5.3.0
+ google_maps_flutter_web 0.3.0+4
+ html 0.15.0
+ js 0.6.3
+ js_wrapping 0.7.3
  matcher 0.12.10 (0.12.11 available)
  meta 1.3.0 (1.7.0 available)
+ sanitize_html 2.0.0
  test_api 0.3.0 (0.4.3 available)
Changed 8 dependencies!

iOS の platform の設定

iOS で Google Maps SDK の最新バージョンを使用するには、プラットフォームの最低バージョンを iOS 11 にする必要があります。ios/Podfile を次のように変更しましょう。

ios/Podfile

# Set platform to 11.0 to enable latest Google Maps SDK
platform :ios, '11.0' # Uncomment and set to 11.

# 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                      // Update from 16 to 20
        targetSdkVersion 30
        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/mainAndroidManifest.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/RunnerAppDelegate.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 キーを追加するには、webindex.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({Key? key}) : super(key: key);

  @override
  _MyAppState 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(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Maps Sample App'),
          backgroundColor: Colors.green[700],
        ),
        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 を追加します。

$ 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!

最後に、開発時用の依存関係として 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:http/http.dart' as http;
import 'package:json_annotation/json_annotation.dart';
import 'package:flutter/services.dart' show rootBundle;

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));
    }
  } catch (e) {
    print(e);
  }

  // Fallback for when the above HTTP request fails.
  return Locations.fromJson(
    json.decode(
      await rootBundle.loadString('assets/locations.json'),
    ),
  );
}

このコードを追加すると、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({Key? key}) : super(key: key);

  @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: const CameraPosition(
            target: LatLng(0, 0),
            zoom: 2,
          ),
          markers: _markers.values.toSet(),
        ),
      ),
    );
  }
}

このコードは次のような処理を行います。

  • _onMapCreated では前述の JSON パース処理コードを使用しており、同コードの読み込みを await するようになっています。その後、返されたデータを使って setState() コールバック内に Marker を作成しています。アプリが新しいマーカーを受け取ると、setState が Flutter に画面をリペイントするよう指示し、各オフィスの位置が表示されます。
  • マーカーは、GoogleMap ウィジェットと関連付けられた Map 内に格納されます。これにより、マーカーが適切な地図とリンクされます(複数の地図にそれぞれ異なるマーカー群を表示する場合もあるため)。

71c460c73b1e061e.png

ここまでの成果のスクリーンショットです。この時点でさまざまな興味深い追加機能を組み込むことも可能です。たとえばオフィスの一覧を表示して、ユーザーがオフィスを選択すればそれに合わせて地図が動いたりズームしたりする、といった機能が考えられますが、これはいわゆる「読者への宿題」の領分になるでしょう。よろしければお試しください。

6 次のステップ

おつかれさまでした

この Codelab はこれで完了です。Google マップを組み込んだ Flutter アプリが完成し、JSON ウェブサービスとやりとりする方法も確認できました。

次のステップ(応用)

この Codelab では、地図上で複数の地点を視覚化するエクスペリエンスを構築しました。これは幅広いユーザーニーズに対応できる機能で、さまざまなモバイルアプリに活用されています。さらなる応用に向けて、次のようなリソースをおすすめします。