将 Google 地图添加到 Flutter 应用

1. 简介

Flutter 是 Google 的移动应用 SDK,旨在以快于以往的速度在 iOS 和 Android 上打造优质的原生体验。

借助 Google 地图 Flutter 插件,您可以基于 Google 地图数据将地图添加到应用中。该插件会自动处理对 Google 地图服务器的访问、地图显示以及对用户手势(例如点击和拖动)的响应。您还可以向地图添加标记。这些对象可提供有关地图位置的更多信息,并允许用户与地图互动。

构建内容

在此 Codelab 中,您将使用 Flutter SDK 构建一个包含 Google 地图的移动应用。您的应用将可以:

  • 显示 Google 地图
  • 从网络服务中检索地图数据
  • 在地图上以标记的形式显示这些数据

什么是 Flutter?

Flutter 具有以下三项核心功能。

  • 开发速度快:借助保留应用状态的热重载,您可以在几毫秒内构建 Android 和 iOS 应用。
  • 富有表现力和灵活性:可快速发布旨在为最终用户提供原生体验的功能。
  • 在 iOS 和 Android 上都可实现原生性能:Flutter 的微件整合了所有关键平台差异(例如滚动、导航、图标和字体方面的差异),以提供完整的原生性能。

Google 地图具有以下优势:

  • 全球覆盖率达到 99%:利用超过 200 个国家和地区的可靠、全面的数据构建应用。
  • 每天更新 2,500 万次:可放心使用准确的实时位置信息。
  • 月活跃用户数达到 10 亿:以 Google 地图的基础架构为依托,从容扩大规模。

此 Codelab 会逐步引导您在 iOS 版和 Android 版 Flutter 应用中打造 Google 地图体验。

学习内容

  • 如何创建新的 Flutter 应用。
  • 如何配置 Google 地图 Flutter 插件。
  • 如何使用来自网络服务的位置数据,向地图添加标记。

此 Codelab 将重点介绍如何将 Google 地图添加到 Flutter 应用。对于不相关的概念,我们仅会略作介绍,同时我们还会提供代码块供您复制和粘贴。

您想通过此 Codelab 学习哪些内容?

我不熟悉这个主题,想好好了解一下。 我对这个主题有所了解,但想复习一下。 我想找到示例代码以用到我的项目中。 我想找到有关特定内容的说明。

2. 设置您的 Flutter 环境

您需要使用两款软件才能完成此 Codelab:Flutter SDK一款编辑器。此 Codelab 假定您使用的是 Android Studio,但您可以使用自己的首选编辑器。

您可以使用以下任意设备运行此 Codelab:

  • 连接到计算机并设为开发者模式的实体设备(Android 或 iOS)。
  • iOS 模拟器(需要安装 Xcode 工具)。
  • Android 模拟器(需要在 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 插件作为依赖项

使用 Pub 软件包可以轻松为 Flutter 应用添加更多功能。在此 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 中使用 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 地图 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 地图 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 地图,您需要按照 Maps SDK for AndroidMaps SDK for iOSMaps JavaScript API 的“使用 API 密钥”部分中的说明,通过 Google Maps Platform 配置 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 是应用初始化过程中的核心单例。

您需要对此文件进行两项更改。首先,添加 #import 语句以提取 Google 地图标头,然后调用 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)
  }
}

为 Web 应用添加 API 密钥

若要向 Web 应用添加 API 密钥,请修改 web 中的 index.html 文件。您需要使用 API 密钥,在 head 部分添加对 Maps JavaScript 脚本的引用。

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 模拟器或 iOS 模拟器。您可以随意修改地图中心位置,使其表示您的家乡或对您而言重要的地点。

$ flutter run

5. 将 Google 办公地点添加到地图上

Google 在世界各地设立了众多办事处,包括北美洲拉丁美洲欧洲亚太地区以及非洲和中东地区。如果您研究一番,便会发现这些地图的好处:它们有一个易于使用的 API 端点,用于提供 JSON 格式的办公地点信息。在这一步中,您需要将这些办公地点添加到地图上,然后使用代码生成工具来解析 JSON。

向项目添加三个新的 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_serializable 来声明用于表示 JSON 文档的对象结构。

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

现在,您的代码应该可以再次进行彻底的分析。接下来,我们应该添加 getGoogleOffices 函数中使用的后备 location.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 Web 服务进行了互动。

其他后续步骤

此 Codelab 打造了一种在地图上直观呈现多个点的体验。许多移动应用都依靠这种功能来满足众多不同的用户需求。下面提供了一些其他资源,可帮助您在这方面更进一步: