iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🗺️

[Flutter] Google Maps for Flutter: Implementation Guide and Various Tips

に公開
2

GoogleMap for Flutter

google_maps_flutter | Flutter Package

This article covers the initial setup, displaying the current location, and using custom markers when creating an app that displays Google Maps in Flutter.

Development Environment

macOS Monterey version 12.1 Apple M1
iOS 15.0 iPhone 13 Pro simulator
Android API 32 simulator
Flutter SDK version: 2.10.3
Dart SDK version: 2.16.1

Environment Setup

Add google_maps_flutter: ^2.1.3 to your pubspec.yaml.

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  ...
  google_maps_flutter: ^2.1.3

Obtain a Google Maps API key in advance from here.

Android

Set the API key in android/app/src/main/AndroidManifest.xml as follows:

<manifest ...
  <application ...
    <meta-data android:name="com.google.android.geo.API_KEY"
               android:value="YOUR KEY HERE"/>

Additionally, you need to set minSdkVersion to 20 or higher in android/app/build.gradle.

android {
    defaultConfig {
        minSdkVersion 20
    }
}

iOS

Add the following to ios/Runner/AppDelegate.swift.

import UIKit
import Flutter
import GoogleMaps // Add

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("YOUR KEY HERE") // Add
    // If the gmscore::renderer::GLState::GenBuffers error is displayed, add the following
    // GMSServices.setMetalRendererEnabled(true) // See Bad Know-how for details
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Implementation

First, try the official sample

I'll try implementing and running the sample from here as it is.

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class MapSample extends StatefulWidget {
  const MapSample({Key? key}) : super(key: key);

  @override
  State<MapSample> createState() => MapSampleState();
}

class MapSampleState extends State<MapSample> {
  final Completer<GoogleMapController> _controller = Completer();

  static const CameraPosition _kGooglePlex = CameraPosition(
    target: LatLng(37.42796133580664, -122.085749655962),
    zoom: 14.4746,
  );

  static const CameraPosition _kLake = CameraPosition(
      bearing: 192.8334901395799,
      target: LatLng(37.43296265331129, -122.08832357078792),
      tilt: 59.440717697143555,
      zoom: 19.151926040649414);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GoogleMap(
        mapType: MapType.hybrid,
        initialCameraPosition: _kGooglePlex,
        onMapCreated: (GoogleMapController controller) {
          _controller.complete(controller);
        },
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _goToTheLake,
        label: const Text('To the lake!'),
        icon: const Icon(Icons.directions_boat),
      ),
    );
  }

  Future<void> _goToTheLake() async {
    final GoogleMapController controller = await _controller.future;
    controller.animateCamera(CameraUpdate.newCameraPosition(_kLake));
  }
}

It's OK if a screen like the one below is displayed.

Display current location

Next, I'll retrieve the current location and try setting it as the initial position when displaying the Google Map.

Necessary Packages

There are several packages for retrieving the current location, but this time I'll use
geolocator | Flutter Package.
Note: I'm also using adaptive_dialog | Flutter Package to display dialogs.

pubspec.yaml
dependencies:
    google_maps_flutter: ^2.1.3
    geolocator: ^8.2.1
    adaptive_dialog: ^1.6.3

Add Required Settings

Android

Add the following permission to AndroidManifest.xml.

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

*Note: Separate configuration is required to run in the background.

iOS

Add the following to Info.plist.

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location when open.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location when in the background.</string>

*Note: Separate configuration is required to run in the background.

Permission Check

enum LocationSettingResult {
  serviceDisabled,
  permissionDenied,
  permissionDeniedForever,
  enabled,
}

// Check permissions related to location information
Future<LocationSettingResult> checkLocationSetting() async {
  final serviceEnabled = await Geolocator.isLocationServiceEnabled();
  if (!serviceEnabled) {
    logger.w('Location services are disabled.');
    return Future.value(LocationSettingResult.serviceDisabled);
  }
  var permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) {
      logger.w('Location permissions are denied.');
      return Future.value(LocationSettingResult.permissionDenied);
    }
  }

  if (permission == LocationPermission.deniedForever) {
    logger.w('Location permissions are permanently denied.');
    return Future.value(LocationSettingResult.permissionDeniedForever);
  }
  return Future.value(LocationSettingResult.enabled);
}

Future<void> recoverLocationSettings(
    BuildContext context, LocationSettingResult locationResult) async {
  if (locationResult == LocationSettingResult.enabled) {
    return;
  }
  final result = await showOkCancelAlertDialog(
    context: context,
    okLabel: 'OK',
    cancelLabel: 'Cancel',
    title: 'xxxxxxx',
    message: 'xxxxxxxxxxxx',
  );
  if (result == OkCancelResult.cancel) {
    logger.w('Cancel recover location settings.');
  } else {
    locationResult == LocationSettingResult.serviceDisabled
        ? await Geolocator.openLocationSettings()
        : await Geolocator.openAppSettings();
  }
}

In the code above, checkLocationSetting verifies the permissions related to location information and returns the result.
The result is defined using a separate enum called LocationSettingResult.
recoverLocationSettings takes the result, shows a message to the user, and encourages them to go to the settings screen to update their settings.

For iOS

  • If "Allow Once" is selected:
    • It doesn't result in "permission denied," but the permission dialog will appear again on the next launch.
  • If "Allow While Using App" is selected:
    • It doesn't result in "permission denied," and the user won't be asked again.
  • If "Don't Allow" is selected:
    • permission == LocationPermission.deniedForever

For Android

  • If "While using the app" is selected:
    • It doesn't result in "permission denied," and the user won't be asked again.
  • If "Only this time" is selected:
    • It doesn't result in "permission denied," but the permission dialog will appear again on the next launch.
  • If "Don't allow" is selected:
    • It becomes permission == LocationPermission.denied.

Also, if "Use location" is turned "OFF" in the settings screen:

Geolocator.isLocationServiceEnabled() will return false.
In this specific case, use Geolocator.openLocationSettings() to display the settings screen.

Sample for Displaying Current Location

Once the preliminary checks are complete, retrieve the current location and display the Google Map at that position.

Future<LatLng> getCurrentLocation() async {
  final position = await Geolocator.getCurrentPosition();
  return LatLng(position.latitude, position.longitude);
}

// Modify the previous sample
class MapSampleState extends State<MapSample> {
  // ....
  @override
  Widget build(BuildContext context) {
    return FutureBuilder<LatLng>(
      future: getCurrentLocation(),
      builder: (BuildContext context, AsyncSnapshot<LatLng> snapshot) {
        if (!snapshot.hasData) {
          return const Center(child: CircularProgressIndicator());
        }
        return GoogleMap(
          mapType: MapType.normal,
          initialCameraPosition: CameraPosition(
              target: snapshot.data ?? defaultLocation, zoom: 17.0),
          myLocationEnabled: true,
          onMapCreated: (GoogleMapController controller) {
            _controller.complete(controller);
          },
        );
      },
    );
  }
}

Displaying Custom Images for Markers

Add Custom Marker Images to your Google Maps in Flutter

If you haven't configured the assets in your pubspec.yaml, do so as follows:

  assets:
    - assets/
    - assets/icons/

In this case, we'll proceed assuming that assets/icons/ic_marker.png has been created and will be used.

The following shows how to modify the previous sample. (Detailed error handling has been omitted.)

class MapSampleState extends State<MapSample> {
  // ....
  BitmapDescriptor? _markerIcon;

  Future<LatLng> _initAsync(BuildContext context) async {
    await _loadPinAsset();

    final result = await checkLocationSetting();
    if (result != LocationSettingResult.enabled) {
      await recoverLocationSettings(context, result);
    }
    return await getCurrentLocation();
  }

  Future<void> _loadPinAsset() async {
    _markerIcon = await BitmapDescriptor.fromAssetImage(
        const ImageConfiguration(size: Size(48, 48)),
        'assets/icons/ic_marker.png');
  }

  Marker _createMarker() {
    return Marker(
      markerId: const MarkerId('marker'),
      position: const LatLng(xxxxx, xxxx),
      icon: _markerIcon ?? BitmapDescriptor.defaultMarker,
      infoWindow: const InfoWindow(title: 'title'),
    );
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<LatLng>(
      future: _initAsync(context),
      builder: (BuildContext context, AsyncSnapshot<LatLng> snapshot) {
        if (!snapshot.hasData) {
          return const Center(child: CircularProgressIndicator());
        }
        return GoogleMap(
          mapType: MapType.normal,
          initialCameraPosition: CameraPosition(
              target: snapshot.data ?? defaultLocation, zoom: 17.0),
          myLocationEnabled: true,
          onMapCreated: (GoogleMapController controller) {
            _controller.complete(controller);
          },
          markers: Set<Marker>.of(<Marker>{_createMarker()}),
        );
      },
    );
  }
}

Displaying Custom Images (SVG) for Markers

Next, I'll try displaying an SVG image as a marker. We'll proceed assuming assets/icons/ic_marker.svg is created.
First, add flutter_svg to your pubspec.yaml.

pubspec.yaml
flutter_svg: ^1.0.3

First, implement the logic to load the SVG image from assets as a BitmapDescriptor.

import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

Future<BitmapDescriptor?> bitmapDescriptorFromSvgAsset(
    BuildContext context, String assetName, {int w = 32, int h = 32}) async {

  String svgString = await DefaultAssetBundle.of(context).loadString(assetName);
  DrawableRoot svgDrawableRoot = await svg.fromSvgString(svgString, assetName);

  MediaQueryData queryData = MediaQuery.of(context);
  double devicePixelRatio = queryData.devicePixelRatio;
  double width = w * devicePixelRatio;
  double height = h * devicePixelRatio;

  ui.Picture picture = svgDrawableRoot.toPicture(size: Size(width, height));

  ui.Image image = await picture.toImage(width.toInt(), height.toInt());
  ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png);
  return bytes != null
      ? BitmapDescriptor.fromBytes(bytes.buffer.asUint8List())
      : null;
}

Simply change the part where the PNG image was being loaded to the following.

  Future<void> _loadPinAsset() async {
    _markerIcon = await bitmapDescriptorFromSvgAsset(
        context, 'assets/icons/ic_marker.svg');
  }

Troubleshooting (Bad Know-how)

  • An Unhandled Exception occurs!
E/flutter ( 5711): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: setState() called after dispose(): _GoogleMapsState#b9489(lifecycle state: defunct, not mounted)
E/flutter ( 5711): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
E/flutter ( 5711): The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
E/flutter ( 5711): This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().

As stated in the error message, the solution is to call dispose() or check the mounted property before calling setState(), such as if (mounted) { setState() }.

  • The iOS simulator occasionally crashes with EXC_BAD_ACCESS

If you check the error log and find entries related to gmscore::renderer::GLState::GenBuffers, adding the following may resolve the issue:

AppDelegate.swift
GMSServices.setMetalRendererEnabled(true)

iOS 14 crash in gmscore::renderer::GLState::GenBuffers [225014752] - Visible to Public - Issue Tracker

Reference URLs

Discussion

めろんぺんめろんぺん

GMSServices.setMetalRendererEnabled(true)
このコードがあってアプリがクラッシュしていましたので、エラーが出ない場合は書かなくてもいいって書いて欲しいです。。
2時間溶かしました笑

slowhandslowhand

コメントありがとうございます。助かります!
記事修正しました。