Добавление Google Maps в приложение Flutter

1. Введение

Flutter — это SDK от Google для мобильных приложений, позволяющий создавать высококачественные нативные приложения для iOS и Android в рекордно короткие сроки.

С помощью плагина Google Maps для Flutter вы можете добавлять в свое приложение карты, основанные на данных Google Maps. Плагин автоматически обрабатывает доступ к серверам Google Maps, отображение карты и реакцию на действия пользователя, такие как щелчки и перетаскивания. Вы также можете добавлять маркеры на карту. Эти объекты предоставляют дополнительную информацию о местоположении на карте и позволяют пользователю взаимодействовать с картой.

Что вы построите

В этом практическом занятии вы создадите мобильное приложение с картой Google, используя Flutter SDK. Ваше приложение будет:

  • Отобразить карту Google
  • Получение картографических данных из веб-сервиса.
  • Отобразите эти данные в виде маркеров на карте.

Скриншот приложения Flutter с запущенной картой Google Maps в симуляторе iPhone, на котором выделен город Маунтин-Вью.

Что такое Flutter?

Flutter обладает тремя основными возможностями.

  • Быстрая разработка : создавайте приложения для Android и iOS за миллисекунды с помощью Stateful Hot Reload.
  • Выразительный и гибкий : быстрая разработка функций с упором на удобство использования для конечных пользователей.
  • Производительность, характерная для нативных приложений, как на iOS, так и на Android : виджеты Flutter учитывают все важные различия платформ — такие как прокрутка, навигация, значки и шрифты — для обеспечения полноценной нативной производительности.

В Google Maps есть:

  • 99% охват мира : Создано на основе надежных и всеобъемлющих данных по более чем 200 странам и территориям.
  • 25 миллионов обновлений ежедневно : полагайтесь на точную информацию о местоположении в режиме реального времени.
  • 1 миллиард ежемесячно активных пользователей : уверенное масштабирование благодаря инфраструктуре Google Maps.

В этом практическом занятии вы узнаете, как создать приложение Flutter, интегрирующее Google Maps, для iOS и Android.

Что вы узнаете

  • Как создать новое Flutter-приложение.
  • Как настроить плагин Google Maps для Flutter.
  • Как добавить маркеры на карту, используя данные о местоположении из веб-сервиса.

В этом практическом занятии мы рассмотрим добавление карты Google в приложение Flutter. Несущественные понятия и блоки кода будут рассмотрены вскользь, и вы сможете просто скопировать и вставить их.

Чему бы вы хотели научиться на этом практическом занятии?

Я новичок в этой теме и хотел бы получить хороший обзор. Я кое-что знаю об этой теме, но хотел бы освежить знания. Мне нужны примеры кода для использования в моём проекте. Мне нужно объяснение одного конкретного момента.

2. Настройте среду Flutter.

Для выполнения этой лабораторной работы вам понадобятся два программных компонента: Flutter SDK и редактор . В данной работе предполагается использование Android Studio, но вы можете использовать любой предпочитаемый вами редактор.

Вы можете выполнить это практическое задание, используя любое из следующих устройств:

  • Физическое устройство (Android или iOS), подключенное к компьютеру и настроенное на режим разработчика.
  • Симулятор iOS. (Требуется установка инструментов Xcode .)
  • Эмулятор Android. (Требуется настройка в Android Studio .)

3. Начало работы

Начало работы с Flutter

Самый простой способ начать работу с Flutter — использовать инструмент командной строки flutter для создания всего необходимого кода, что значительно упрощает начало работы.

$ flutter create google_maps_in_flutter --platforms android,ios,web
Creating project google_maps_in_flutter...
Resolving dependencies in `google_maps_in_flutter`... 
Downloading packages... 
Got dependencies in `google_maps_in_flutter`.
Wrote 81 files.

All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev

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 Maps Flutter в качестве зависимости.

Добавить дополнительные возможности в приложение Flutter легко с помощью пакетов Pub . В этом практическом занятии вы познакомитесь с плагином Google Maps для Flutter , выполнив следующую команду из каталога проекта.

$ cd google_maps_in_flutter
$ flutter pub add google_maps_flutter
Resolving dependencies... 
Downloading packages... 
+ csslib 1.0.0
+ flutter_plugin_android_lifecycle 2.0.19
+ flutter_web_plugins 0.0.0 from sdk flutter
+ google_maps 7.1.0
+ google_maps_flutter 2.6.1
+ google_maps_flutter_android 2.8.0
+ google_maps_flutter_ios 2.6.0
+ google_maps_flutter_platform_interface 2.6.0
+ google_maps_flutter_web 0.5.7
+ html 0.15.4
+ js 0.6.7 (0.7.1 available)
+ js_wrapping 0.7.4
  leak_tracker 10.0.4 (10.0.5 available)
  leak_tracker_flutter_testing 3.0.3 (3.0.5 available)
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.12.0 (1.14.0 available)
+ plugin_platform_interface 2.1.8
+ sanitize_html 2.1.0
+ stream_transform 2.1.0
  test_api 0.7.0 (0.7.1 available)
+ web 0.5.1
Changed 16 dependencies!
6 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Настройка platform iOS

Для установки последней версии Google Maps SDK на iOS требуется минимальная версия платформы iOS 14. Измените верхнюю часть конфигурационного файла ios/Podfile следующим образом.

ios/Podfile

# Google Maps SDK requires platform version 14
# https://developers.google.com/maps/flutter-package/config#ios
platform :ios, '14.0'


# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

Настройка Android minSDK

Для использования Google Maps SDK на Android необходимо установить значение minSdk равным 21. Измените конфигурационный файл 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"
        // Minimum Android version for Google Maps SDK
        // https://developers.google.com/maps/flutter-package/config#android
        minSdk = 21
        targetSdk = flutter.targetSdkVersion
        versionCode = flutterVersionCode.toInteger()
        versionName = flutterVersionName
    }

}

4. Добавление Google Maps в приложение.

Все дело в ключах API.

Для использования Google Maps в вашем Flutter-приложении необходимо настроить проект API с помощью платформы Google Maps , следуя инструкциям по использованию ключа API в Maps SDK для Android , Maps SDK для iOS и Maps JavaScript API . Имея ключи API, выполните следующие шаги для настройки приложений Android и iOS.

Добавление ключа API для Android-приложения

Чтобы добавить ключ API в приложение Android, отредактируйте файл AndroidManifest.xml в папке android/app/src/main . Добавьте одну запись meta-data содержащую ключ API, созданный на предыдущем шаге, в узел application .

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="google_maps_in_flutter"
        android:name="${applicationName}"
        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:exported="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            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"
              />
            <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>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>

Добавление ключа API для iOS-приложения

Чтобы добавить ключ API в приложение iOS, отредактируйте файл AppDelegate.swift в ios/Runner . В отличие от Android, добавление ключа API на iOS требует внесения изменений в исходный код приложения Runner. AppDelegate — это основной синглтон, являющийся частью процесса инициализации приложения.

Внесите два изменения в этот файл. Во-первых, добавьте оператор #import для подкачки заголовков Google Maps, а затем вызовите метод ` provideAPIKey() ` синглтона ` GMSServices . Этот ключ API позволит Google Maps корректно отображать фрагменты карты.

ios/Runner/AppDelegate.swift

import Flutter
import UIKit
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")               // Add this line

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

Добавление ключа API для веб-приложения

Чтобы добавить ключ API в веб-приложение, отредактируйте файл index.html в web . Добавьте ссылку на JavaScript-скрипт Maps в раздел <head>, указав свой ключ API.

web/index.html

<!DOCTYPE html>
<html>
<head>
  <!--
    If you are serving your web app in a path other than the root, change the
    href value below to reflect the base path you are serving from.

    The path provided below has to start and end with a slash "/" in order for
    it to work correctly.

    For more details:
    * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

    This is a placeholder for base href that will be replaced by the value of
    the `--base-href` argument provided to `flutter build`.
  -->
  <base href="$FLUTTER_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">

  <!-- Favicon -->
  <link rel="icon" type="image/png" href="favicon.png"/>

  <!-- 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>
<body>
  <script src="flutter_bootstrap.js" async></script>
</body>
</html>

Вывод карты на экран

Теперь пришло время вывести карту на экран. Замените содержимое файла 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,
          ),
        ),
      ),
    );
  }
}

Запуск приложения

Запустите приложение Flutter на iOS или Android, чтобы увидеть единую карту с центром в Портленде. В качестве альтернативы можно запустить эмулятор Android или симулятор iOS. Вы можете изменить центр карты, чтобы он отображал ваш родной город или место, которое для вас важно.

$ flutter run

Скриншот приложения Flutter с картой Google, работающего в симуляторе iPhone.

Скриншот приложения Flutter с картой Google, работающего в эмуляторе Android.

5. Добавьте Google на карту.

У Google множество офисов по всему миру: в Северной Америке , Латинской Америке , Европе , Азиатско-Тихоокеанском регионе , Африке и на Ближнем Востоке . Преимущество этих карт, если вы их изучите, заключается в наличии легкодоступного API-интерфейса для предоставления информации о местоположении офисов в формате JSON. На этом шаге вы разместите эти офисы на карте. На этом шаге вы будете использовать генерацию кода для анализа JSON.

Добавьте в проект три новые зависимости Flutter следующим образом: пакет http для упрощения выполнения HTTP-запросов, json_serializable и json_annotation для объявления структуры объектов, представляющих JSON-документы, и build_runner для поддержки генерации кода.

$ flutter pub add http json_annotation json_serializable dev:build_runner
Resolving dependencies... 
Downloading packages... 
+ _fe_analyzer_shared 67.0.0 (68.0.0 available)
+ analyzer 6.4.1 (6.5.0 available)
+ args 2.5.0
+ build 2.4.1
+ build_config 1.1.1
+ build_daemon 4.0.1
+ build_resolvers 2.4.2
+ build_runner 2.4.9
+ build_runner_core 7.3.0
+ built_collection 5.1.1
+ built_value 8.9.2
+ checked_yaml 2.0.3
+ code_builder 4.10.0
+ convert 3.1.1
+ crypto 3.0.3
+ dart_style 2.3.6
+ file 7.0.0
+ fixnum 1.1.0
+ frontend_server_client 4.0.0
+ glob 2.1.2
+ graphs 2.3.1
+ http 1.2.1
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ io 1.0.4
  js 0.6.7 (0.7.1 available)
+ json_annotation 4.9.0
+ json_serializable 6.8.0
  leak_tracker 10.0.4 (10.0.5 available)
  leak_tracker_flutter_testing 3.0.3 (3.0.5 available)
+ logging 1.2.0
  material_color_utilities 0.8.0 (0.11.1 available)
  meta 1.12.0 (1.14.0 available)
+ mime 1.0.5
+ package_config 2.1.0
+ pool 1.5.1
+ pub_semver 2.1.4
+ pubspec_parse 1.2.3
+ shelf 1.4.1
+ shelf_web_socket 1.0.4
+ source_gen 1.5.0
+ source_helper 1.3.4
  test_api 0.7.0 (0.7.1 available)
+ timing 1.0.1
+ typed_data 1.3.2
+ watcher 1.1.0
+ web_socket_channel 2.4.5
+ yaml 3.1.2
Changed 42 dependencies!
8 packages have newer versions incompatible with dependency constraints.
Try `flutter pub outdated` for more information.

Парсинг JSON с генерацией кода

Возможно, вы заметили, что данные в формате JSON, возвращаемые API-интерфейсом, имеют регулярную структуру. Было бы удобно сгенерировать код для преобразования этих данных в объекты, которые можно использовать в программном коде.

В каталоге 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 следующим образом:

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

Теперь ваш код должен снова корректно анализироваться. Далее следует добавить резервный файл locations.json, используемый в функции getGoogleOffices . Одна из причин включения этого резервного файла заключается в том, что статические данные, загружаемые в этой функции, предоставляются без заголовков CORS и, следовательно, не будут загружаться в веб-браузере. Приложения Flutter для Android и iOS не нуждаются в заголовках CORS, но доступ к мобильным данным может быть капризным даже в лучшие времена.

Перейдите по ссылке https://about.google/static/data/locations.json в вашем браузере и сохраните содержимое в каталог assets. В качестве альтернативы вы можете использовать командную строку следующим образом.

$ 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

Теперь, когда вы скачали файл ресурсов, добавьте его в раздел flutter вашего файла pubspec.yaml .

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 его загрузки. Затем полученные данные используются для создания Marker внутри функции обратного вызова setState() . Как только приложение получает новые маркеры, setState запускает перерисовку экрана Flutter, в результате чего отображаются местоположения офисов.
  • Маркеры хранятся на Map , связанной с виджетом GoogleMap . Это связывает маркеры с нужной картой. Конечно, можно иметь несколько карт и отображать на каждой из них разные маркеры.

Скриншот приложения Flutter с запущенной картой Google Maps в симуляторе iPhone, на котором выделен город Маунтин-Вью.

Вот скриншот того, чего вы добились. На данном этапе можно внести множество интересных дополнений. Например, можно добавить список офисов, который будет перемещать и масштабировать карту при щелчке пользователя по офису, но, как говорится, это задача на усмотрение читателя!

6. Дальнейшие шаги

Поздравляем!

Вы завершили практическое задание и создали Flutter-приложение с картой Google! Вы также взаимодействовали с веб-сервисом JSON.

Дальнейшие шаги

В этом практическом занятии создан интерфейс для визуализации множества точек на карте. Существует ряд мобильных приложений, которые используют эту возможность для удовлетворения самых разных потребностей пользователей. Есть и другие ресурсы, которые помогут вам продвинуться дальше: