Cómo agregar Google Maps a una app creada con Flutter

1. Introducción

Flutter es un SDK de apps para dispositivos móviles de Google que permite crear experiencias nativas de alta calidad en iOS y Android en tiempo récord.

Con el complemento de Google Maps para Flutter, puedes agregar mapas basados en datos de Google Maps a tu aplicación. El complemento administra automáticamente el acceso a los servidores de Google Maps, la visualización de los mapas y la respuesta a gestos del usuario, como cuando se hace clic o se arrastra algún elemento. También puedes agregar marcadores a tu mapa. Estos objetos proporcionan información adicional de las ubicaciones en el mapa y permiten al usuario interactuar con este.

Qué compilarás

En este codelab, utilizarás el SDK de Flutter a fin de compilar una app para dispositivos móviles que muestre un mapa de Google Maps. Tu app hará lo siguiente:

  • Mostrará un mapa de Google Maps.
  • Recuperará datos de mapas de un servicio web.
  • Mostrará esos datos como marcadores en el mapa.

¿Qué es Flutter?

Flutter tiene tres características principales:

  • Desarrollo ágil: Compila tus aplicaciones para iOS y Android en cuestión de milisegundos gracias al proceso de recarga en caliente con estado.
  • Expresivo y flexible: Incluye funciones rápidamente haciendo hincapié en las experiencias nativas del usuario final.
  • Rendimiento nativo en iOS y Android: Los widgets de Flutter incorporan todas las diferencias fundamentales de cada plataforma, como el desplazamiento, la navegación, los íconos y las fuentes, para proporcionar un rendimiento nativo completo.

Google Maps ofrece lo siguiente:

  • Cobertura del 99% del mundo: Compila software con datos exhaustivos y confiables de más de 200 países y territorios.
  • 25 millones de actualizaciones diarias: Recibe información precisa y en tiempo real acerca de las ubicaciones.
  • 1,000 millones de usuarios activos por mes: Aumenta el alcance de tu app con confianza y con el respaldo de la infraestructura de Google Maps.

En este codelab, se explica cómo crear una experiencia de Google Maps en una app creada con Flutter para iOS y Android.

Qué aprenderás

  • Cómo crear una nueva aplicación con Flutter
  • Cómo configurar el complemento de Google Maps para Flutter
  • Cómo agregar marcadores a un mapa con los datos de ubicación de un servicio web

En este codelab, nos centraremos en cómo agregar un mapa de Google Maps a una app creada con Flutter. No ahondaremos en conceptos ni bloques de código que no sean relevantes, los cuales solo se proporcionan para que los copies y pegues.

¿Qué te gustaría aprender en este codelab?

No sé nada del tema y me gustaría obtener una buena descripción general. Sé algo del tema y me gustaría repasar mis conocimientos. Estoy buscando un código de ejemplo para utilizar en un proyecto propio. Estoy buscando una explicación sobre algo específico.

2. Configura tu entorno de Flutter

Para completar este lab, necesitas dos software: el SDK de Flutter y un editor. En este codelab, se presupone que utilizarás Android Studio, pero puedes optar por el editor que desees.

Puedes ejecutar este codelab en cualquiera de los siguientes dispositivos:

  • Un dispositivo físico (iOS o Android) conectado a tu computadora y configurado en modo de desarrollador
  • El simulador de iOS (requiere instalar herramientas de Xcode)
  • El emulador de Android (requiere configuración en Android Studio)

3. Cómo comenzar

Cómo comenzar a utilizar Flutter

La manera más sencilla de comenzar a utilizar Flutter es emplear su herramienta de línea de comandos para crear todo el código necesario a fin de generar una experiencia inicial simple.

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

Cómo agregar el complemento de Google Maps para Flutter como dependencia

Agregar funciones adicionales a una app creada con Flutter es muy fácil gracias a los paquetes de Pub. En este codelab, incorporarás el complemento de Google Maps para Flutter. Para ello, debes ejecutar el siguiente comando desde el directorio del proyecto:

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

También veremos cómo utilizar Google Maps en Flutter para la Web. Sin embargo, la versión web del complemento aún no está federada, por lo que también deberás agregarla al proyecto.

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

Cómo configurar platform de iOS

Para obtener la versión más reciente del SDK de Google Maps en iOS, se requiere una versión mínima de la plataforma de iOS 11. Modifica el código de ios/Podfile como se indica a continuación.

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'

Cómo configurar minSDK de Android

Para utilizar el SDK de Google Maps en Android, se requiere configurar minSDK en 20. Modifica el código de android/app/build.gradle como se indica a continuación.

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. Cómo agregar Google Maps a la app

Las claves de API son fundamentales

Para utilizar Google Maps en tu app creada con Flutter, debes configurar un proyecto de API con Google Maps Platform. Para ello, sigue las instrucciones sobre cómo usar las claves de API del SDK de Maps para Android, del SDK de Maps para iOS y de la API de Maps JavaScript. Con las claves de API a mano, sigue los pasos que se indican a continuación a fin de configurar las aplicaciones para iOS y Android.

Cómo agregar una clave de API a una app para Android

Si deseas agregar una clave de API a una app para Android, edita el archivo AndroidManifest.xml en android/app/src/main. Agrega una sola entrada meta-data que contenga la clave de API creada en el paso anterior dentro del nodo 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>

Cómo agregar una clave de API a una app para iOS

Si deseas agregar una clave de API a una app para iOS, edita el archivo AppDelegate.swift en ios/Runner. A diferencia de lo que sucede en Android, para agregar una clave de API en iOS, se deben hacer cambios en el código fuente de la app de Runner. AppDelegate es el singleton principal que forma parte del proceso de inicialización de apps.

Debes hacer dos cambios en este archivo. Primero, agrega una sentencia #import para extraer los encabezados de Google Maps y, luego, llama al método provideAPIKey() del singleton GMSServices. Esta clave de API permite a Google Maps mostrar correctamente los mosaicos del mapa.

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

Cómo agregar una clave de API a una app web

Para agregar una clave de API a una app web, edita el archivo index.html en web. Agrega una referencia a la secuencia de comandos de Maps JavaScript en la sección <head> con tu clave 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>

Cómo mostrar un mapa en la pantalla

Ahora es el momento de mostrar un mapa en la pantalla. Actualiza lib/main.dart de la siguiente manera:

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

Cómo ejecutar la app

Ejecuta la app creada con Flutter en iOS o Android para ver una sola vista de mapa centrada en Portland. También puedes ejecutar un emulador de Android o un simulador de iOS. Puedes modificar el centro del mapa para que sea tu ciudad o el lugar que desees.

$ flutter run

5. Coloca a Google en el mapa

Google tiene muchas oficinas en distintas partes del mundo, desde Norteamérica, Latinoamérica, Europa y Asia-Pacífico hasta África y Oriente Medio. Lo bueno de estos mapas, si los investigas, es que tienen un extremo de API que se puede utilizar fácilmente para proporcionar información de la ubicación de las oficinas en formato JSON. En este paso, colocarás las ubicaciones de esas oficinas en el mapa y generarás un código para analizar datos JSON.

Agrega tres dependencias nuevas de Flutter al proyecto de la siguiente manera. Primero, agrega el paquete http para realizar solicitudes HTTP con facilidad.

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

A continuación, agrega json_serializable a fin de declarar la estructura de objetos para representar los 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 último, agrega build_runner como una dependencia de tiempo de desarrollo. Esta se utilizará para generar código más adelante en este paso.

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

Cómo analizar datos JSON a través de la generación de código

Probablemente adviertas que los datos JSON resultantes provenientes del extremo de API tienen una estructura regular. Por eso, sería conveniente generar el código necesario para organizar esos datos en objetos que puedas utilizar en el código.

En el directorio lib/src, crea un archivo locations.dart y describe la estructura de los datos JSON resultantes de la siguiente manera:

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

Tras agregar este código, tu IDE (si utilizas uno) debería mostrar partes con un subrayado ondulado de color rojo, ya que hace referencia a un archivo del mismo nivel inexistente, locations.g.dart.. Este archivo generado realiza conversiones entre estructuras JSON sin tipo y objetos con nombre. Ejecuta build_runner para crearlo:

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

Ahora, tu código debería volver a analizarse correctamente. A continuación, debemos agregar el archivo de resguardo locations.json que se utiliza en la función getGoogleOffices. Una de las razones para incluir este archivo de resguardo es que los datos estáticos cargados en esta función se entregan sin encabezados CORS y, por lo tanto, no pueden cargarse en un navegador web. Las apps creadas con Flutter para iOS y Android no requieren encabezados CORS, pero el acceso a datos móviles puede ser complejo en el mejor de los casos.

Ve a https://about.google/static/data/locations.json en tu navegador y guarda el contenido en el directorio de recursos. Como alternativa, puedes utilizar la línea de comandos de la siguiente manera:

$ 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

Una vez que descargues el archivo de recursos, agrégalo a la sección flutter de tu archivo pubspec.yaml.

pubspec.yaml

flutter:
  uses-material-design: true

  assets:
    - assets/locations.json

Modifica el archivo main.dart para solicitar los datos del mapa y, luego, utiliza la información resultante para agregar las oficinas al 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(),
        ),
      ),
    );
  }
}

Este código realiza varias operaciones:

  • En _onMapCreated, utiliza el código de análisis de JSON del paso anterior y espera (await) hasta que se completa la carga. Luego, utiliza los datos resultantes para crear marcadores (Marker) dentro de una devolución de llamada setState(). Una vez que la app recibe marcadores nuevos, setState indica a Flutter que vuelva a procesar la imagen de la pantalla para mostrar las ubicaciones de las oficinas.
  • Los marcadores se almacenan en un mapa (Map) asociado con el widget de GoogleMap, lo que permite vincularlos al mapa correcto. Sin duda, podrías tener varios mapas y mostrar diferentes marcadores en cada uno.

71c460c73b1e061e.png

Esta es una captura de pantalla de lo que lograste. En este punto, se pueden agregar muchas opciones interesantes. Por ejemplo, podrías agregar una vista de lista de las oficinas y que el mapa se mueva y se acerque cuando el usuario haga clic en una de las oficinas. No obstante, este ejercicio queda a cargo del lector.

6. Próximos pasos

¡Felicitaciones!

Completaste el codelab y compilaste una app con Flutter con un mapa de Google Maps. También interactuaste con un servicio web JSON.

Otros próximos pasos

En este codelab, se compiló una experiencia para visualizar una serie de puntos en un mapa. Hay varias apps para dispositivos móviles que expanden esta función a fin de satisfacer diversas necesidades de los usuarios. Existen otros recursos que pueden ayudarte con ello: