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 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/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({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
内に格納されます。これにより、マーカーが適切な地図とリンクされます(複数の地図にそれぞれ異なるマーカー群を表示する場合もあるため)。
ここまでの成果のスクリーンショットです。この時点でさまざまな興味深い追加機能を組み込むことも可能です。たとえばオフィスの一覧を表示して、ユーザーがオフィスを選択すればそれに合わせて地図が動いたりズームしたりする、といった機能が考えられますが、これはいわゆる「読者への宿題」の領分になるでしょう。よろしければお試しください。
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 に公開している記事がおすすめです。