Adicionar o Google Maps a um app do Flutter

1. Introdução

O Flutter é o SDK de apps para dispositivos móveis do Google. Ele cria experiências nativas de alta qualidade no iOS e no Android em tempo recorde.

Com o plug-in do Google Maps para Flutter, você pode adicionar mapas ao seu aplicativo com base nos dados do Google Maps. O plug-in controla automaticamente o acesso aos servidores do Google Maps, a exibição de mapas e a resposta aos gestos do usuário, como cliques e arrastos. Você também pode adicionar marcadores ao mapa. Esses objetos exibem informações adicionais sobre os locais do mapa e permitem que o usuário interaja com ele.

O que você criará

Neste codelab, você criará um app para dispositivos móveis que traz um mapa do Google Maps usando o SDK do Flutter. Esse app vai:

  • exibir um mapa do Google Maps;
  • extrair dados de mapa de um serviço da Web;
  • exibir esses dados na forma de marcadores no mapa.

O que é o Flutter?

O Flutter tem três recursos principais.

  • Rápido para desenvolver: crie seus aplicativos para Android e iOS em milissegundos com a recarga automática com estado.
  • Expressivo e flexível: envie recursos rapidamente com foco nas experiências nativas do usuário final.
  • Desempenho nativo no iOS e no Android: os widgets do Flutter incorporam todas as diferenças críticas da plataforma, como rolagem, navegação, ícones e fontes, proporcionando um desempenho nativo completo.

Veja o que o Google Maps oferece:

  • 99% de cobertura no mundo: crie com dados confiáveis e abrangentes sobre mais de 200 países e territórios.
  • 25 milhões de atualizações diárias: conte com informações de local precisas e em tempo real.
  • Um bilhão de usuários ativos por mês: escalone com confiança usando a infraestrutura do Google Maps.

Este codelab mostra como criar uma experiência do Google Maps em um app do Flutter para iOS e Android. Para você se aprofundar no tema, este codelab traz links para repositórios e bibliotecas. A maioria dessas páginas está em inglês.

O que você aprenderá

  • Como criar um novo aplicativo do Flutter
  • Como configurar um plug-in do Google Maps para Flutter
  • Como adicionar marcadores a um mapa usando dados de local de um serviço da Web

O foco deste codelab é adicionar um mapa do Google a um app do Flutter. Conceitos e blocos de códigos não relevantes serão resumidos e apresentados para que você apenas copie e cole.

O que você quer aprender com este codelab?

Ainda não conheço bem o assunto e quero ter uma boa visão geral. Conheço um pouco sobre esse assunto, mas quero me atualizar. Estou procurando exemplos de código para usar no meu projeto. Estou procurando uma explicação de algo específico.

2. Configurar o ambiente do Flutter

Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor. Este codelab irá trabalhar com o Android Studio, mas você pode usar o editor que preferir.

É possível completar este codelab usando qualquer um dos seguintes dispositivos:

  • Dispositivo físico (Android ou iOS) conectado ao computador e configurado para o modo de desenvolvedor
  • Simulador de iOS (requer instalar o Xcode)
  • Android Emulator (requer configuração no Android Studio)

3. Primeiros passos

Primeiros passos com o Flutter

A maneira mais fácil de começar com o Flutter é usar a ferramenta de linha de comando dele e criar todo o código necessário para uma primeira experiência.

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

Adicionar o plug-in do Google Maps ao Flutter como uma dependência

É fácil adicionar outros recursos a um app do Flutter usando os pacotes do Pub. Neste codelab, você insere o plug-in do Flutter do Google Maps executando o seguinte comando a partir do diretório do projeto:

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

Este codelab também aborda como usar o Google Maps no Flutter para a Web. No entanto, a versão da Web do plug-in ainda não é federada, então você também precisa adicioná-lo ao seu projeto.

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

Como configurar o platform do iOS

Para usar a versão mais recente do SDK do Google Maps no iOS, é necessário ter no mínimo a versão do iOS 11 da plataforma. Modifique o ios/Podfile da seguinte maneira:

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'

Como configurar o minSDK Android

Para usar o SDK do Google Maps no Android, defina minSDK como 20. Modifique o android/app/build.gradle conforme exibido abaixo.

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. Adicionar o Google Maps ao app

O que importa são as chaves de API

Para usar o Google Maps no seu app do Flutter, é necessário configurar um projeto de API com a Plataforma Google Maps, seguindo as instruções de como usar chaves de API no SDK do Maps para Android, no SDK do Maps para iOS e no SDK da API Maps JavaScript. Com as chaves de API em mãos, execute as etapas a seguir para configurar os aplicativos Android e iOS.

Adicionar uma chave de API a um app Android

Para adicionar uma chave de API ao app Android, edite o arquivo AndroidManifest.xml em android/app/src/main. Adicione uma única entrada meta-data contendo a chave de API criada na etapa anterior dentro do nó application.

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>

Adicionar uma chave de API a um app iOS

Para adicionar uma chave de API ao app iOS, edite o arquivo AppDelegate.swift em ios/Runner. Ao contrário do Android, adicionar uma chave de API no iOS exige mudanças no código-fonte do app Runner. O AppDelegate é o Singleton principal que faz parte do processo de inicialização do app.

Faça duas alterações nesse arquivo. Primeiro, adicione uma instrução #import para extrair os cabeçalhos do Google Maps e, em seguida, chame o método provideAPIKey() do Singleton GMSServices. Essa chave de API permite que o Google Maps exiba corretamente os blocos de mapas.

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

Adicionar uma chave de API a um app da Web

Para adicionar uma chave de API ao app da Web, edite o arquivo index.html em web. Adicione uma referência ao script de JavaScript do Google Maps na seção <head> incluindo sua chave de 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>

Mostrar um mapa na tela

Agora, é hora de mostrar um mapa na tela. Atualize lib/main.dart desta maneira:

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,
          ),
        ),
      ),
    );
  }
}

Executar o app

Execute o app do Flutter no iOS ou Android para ter uma única visualização de mapa, centralizada em Portland. Outra opção é executar um emulador do Android ou um simulador de iOS. Você pode modificar o centro do mapa para representar sua cidade ou algum lugar importante para você.

$ flutter run

5. Colocar o Google no mapa

O Google tem muitos escritórios no mundo todo, desde a América do Norte, América Latina, Europa e Ásia-Pacífico até a África e o Oriente Médio. Se você investigar esses mapas, vai ver que eles têm um endpoint de API fácil de usar para fornecer as informações do local dos escritórios no formato JSON. Nesta etapa, você irá colocar esses escritórios no mapa, além de usar a geração de código para analisar o JSON.

Adicione três novas dependências do Flutter ao projeto da seguinte forma: Primeiro, adicione o pacote http para facilitar as solicitações 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!

Em seguida, adicione json_serializing para declarar a estrutura de objetos e representar documentos 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!

Por fim, adicione build_runner como uma dependência do tempo de desenvolvimento. Ele será usado para a geração de código mais adiante nesta etapa.

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

Como analisar JSON com a geração de código

Observe que os dados JSON retornados do endpoint de API têm uma estrutura regular. Seria interessante gerar o código para organizar esses dados em objetos que você pode usar no código.

No diretório lib/src, crie um arquivo locations.dart e descreva a estrutura dos dados JSON retornados da seguinte forma:

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'),
    ),
  );
}

Depois de adicionar esse código, o ambiente de desenvolvimento integrado (se você estiver usando um) exibirá alguns destaques em vermelho, já que ele faz referência a um arquivo irmão que não existe, o locations.g.dart.. Esse arquivo gerado é convertido entre estruturas JSON não digitadas e objetos nomeados. Crie-o executando o 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)

Agora, seu código será analisado novamente sem erros. Em seguida, precisamos adicionar o arquivo substituto locations.json usado na função getGoogleOffices. Uma das razões para incluir esse substituto é que os dados estáticos carregados nesta função são veiculados sem cabeçalhos CORS e, portanto, não são carregados em um navegador da Web. Os apps do Flutter para Android e iOS não precisam de cabeçalhos CORS, mas o acesso a dados móveis pode ser complicado.

Acesse https://about.google/static/data/locations.json no navegador e salve o conteúdo no diretório de assets. Outra opção é usar a linha de comando da seguinte forma:

$ 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

Agora que você já fez o download do arquivo de assets, adicione-o à seção do Flutter do arquivo pubspec.yaml.

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/locations.json

Modifique o arquivo main.dart para solicitar os dados do mapa e use as informações retornadas para adicionar os escritórios ao mapa:

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(),
        ),
      ),
    );
  }
}

Esse código executa várias operações:

  • Em _onMapCreated, ele usa o código de análise JSON da etapa anterior, aguardando (await) até que ele seja carregado. Em seguida, usará os dados retornados para criar marcadores (Marker) dentro de um callback setState(). Quando o app recebe novos marcadores, o setState sinaliza o Flutter para repintar a tela, exibindo os locais dos escritórios.
  • Os marcadores são armazenados em um Map associado ao widget GoogleMap. Isso vincula os marcadores ao mapa correto. Obviamente, você pode ter vários mapas e exibir marcadores diferentes em cada um.

71c460c73b1e061e.png

Veja uma captura de tela do que você fez hoje. Há muitas coisas interessantes que podem ser adicionadas a partir de agora. Por exemplo, você pode adicionar uma visualização dos escritórios em lista, que se move e amplia o mapa quando o usuário clica em um deles, mas o exercício fica para você.

6. Próximas etapas

Parabéns!

Você concluiu o codelab e criou um app do Flutter com um mapa do Google. Você também interagiu com um serviço da Web JSON.

Etapas adicionais

Este codelab criou uma experiência para visualizar uma série de pontos em um mapa. Há diversos aplicativos para dispositivos móveis desenvolvidos com esse recurso a fim de atender a muitas necessidades diferentes dos usuários. Existem outros recursos que podem ajudar você a fazer isso: