FlutterのGoogle MapでMarkerを動かしてみる

Mobility TechnologiesでFlutterエンジニアとして働いているTomiと申します。

タクシーアプリ「GO」ではGoogle MapにMarker(Taxi)を動かす場合が多くあります。Flutterではどのように動かせるのかを調査しましたので、この内容を共有します。

目標

Google Map上のMarkerを移動するとき、ネイティブ(Android, iOS)側では表示しているMarkerを参照して、そのpositionを少しつづ変更することで対応できます。
しかしながら、Flutterでは表示しているMarkerを参照することはできません。
本記事では、FlutterでMarkerの移動をどのように実現するのかを解説します。

事前準備

  • Google Map Api Key
    • Google Map を表示するためのAPIキーが必要となります。
    • 発行方法はこちらをみてください。

Google Mapを表示

ライブラリインストール

Google Mapを表示するために、google_maps_flutterライブラリ「22年11月現在v2.2.1」を使います。

$ flutter pub add google_maps_flutter

上記のコマンドを実行してライブラリをインストールします。

そして各プラットフォーム側で設定が必要です。

Android設定方法

Flutterプロジェクトを立ち上げるとAndroidminSdkVersion16に設定されていますが、Google Mapでは20以上求めており、20以上で設定する必要がります。

// android/app/build.gradle
android {
    defaultConfig {
        // ...省略
        minSdkVersion 20
        // ...省略
    }
}

そしてAndroidManifest.xmlGoogle Map Api Keyを設定したら完了です。

<!-- android/app/src/main/AndroidManifest.xml -->
<application>
     ...省略
   <meta-data android:name="com.google.android.geo.API_KEY"
       android:value="####"/>
</application>

iOS設定方法

import UIKit
import Flutter
import GoogleMaps // NEW[1]

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("YOUR KEY HERE") // NEW[2]
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

上記のNEW[1]にimport文を追加してNEW[2]Google Map Api Keyを入れてください。

FlutterにGoogleMap Widgetを表示する

// lib/main.dart

import 'package:google_maps_flutter/google_maps_flutter.dart';

...省略

GoogleMap(
  mapType: MapType.normal,
  initialCameraPosition: const CameraPosition(
    target: LatLng(35.68165450744266, 139.76708188461404), // TokyoStation
    zoom: 14,
  ),
);
  • initialCameraPosition : 最初にカメラで表示される位置
  • mapType:マップの表現タイプ

結果

GoogleMap Widgetを表示したスクリーンショット

Flutterアプリを起動するとGoogle Mapが表示していることを確認出来ます。

Markerを表示

GoogleMap(
  mapType: MapType.normal,
  initialCameraPosition: _tokyoStation,
  markers: <Marker>{
    const Marker(
      markerId: MarkerId('driverMarker'),
      position: LatLng(35.68128517123827, 139.76714151602386),
    )
  },
),

GoogleMap WidgetmarkersパラメーターにMarkerを入れると表示されます。

  • markerId : markersの中でユニークなidを設定する必要があります。
  • position : Markerを配置する位置情報です。

Markerを動かす

FlutterでMarkerを動かせるためには少しづつ位置を修正したMarkerを入れたGoogleMapをリビルドします。

今回はTweenアニメーションを使って実装しました。

Marker? _marker;

Future<void> _run() async {
  final animationController = AnimationController(
    duration: const Duration(seconds: 5),
    vsync: this,
  );

  Tween<double> tween = Tween(begin: 0, end: 1);

  _animation = tween.animate(animationController)
    ..addListener(() async {
      final v = _animation!.value;
      double lng = v * _endLng + (1 - v) * _startLng;
      double lat = v * _endLat + (1 - v) * _startLat;
      setState(() {
        _marker = Marker(
          markerId: const MarkerId('driverMarker'),
          position: LatLng(lat, lng),
        );
      });
    });

  animationController.forward();
}

上記の_run関数を実行するとアニメーションが動いて_marker変数を少しつづ目的地まで位置情報を更新します。

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: GoogleMap(
      mapType: MapType.normal,
      initialCameraPosition: _tokyoStation,
      markers: <Marker>{if (_marker != null) _marker!},
    ),
        floatingActionButton: FloatingActionButton(
        onPressed: _run,
        child: const Text('Start'),
      ),
    floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
  );
}

この_markerをGoogleMapのパラメーターで入れておくとMarkerが動くことを確認できます。

しかしながら、上記のコードはMarkerが動く際に、全てのGoogleMapのビルド関数でリビルドが走ってしまうため、性能的に良くないです。この問題を解決するためにリビルドするWidgetを絞る必要がありましてStreamBuilderを使って制限しました。

final _markerStreamController = StreamController<Marker>();
StreamSink<Marker> get _markerSink => _markerStreamController.sink;
Stream<Marker> get _markerStream => _markerStreamController.stream;


@override
Widget build(BuildContext context) {
  return Scaffold(
    body: StreamBuilder<Marker>(
      stream: _markerStream,
      builder: (context, snapshot) {
        final maker = snapshot.data;
        return GoogleMap(
          mapType: MapType.normal,
          initialCameraPosition: _tokyoStation,
          markers: <Marker>{if (maker != null) maker},
        );
      },
    ),
        floatingActionButton: FloatingActionButton(
        onPressed: _run,
        child: const Text('Start'),
      ),
    floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
  );
}

Future<void> _run() async {
  final animationController = AnimationController(
    duration: const Duration(seconds: 5),
    vsync: this,
  );

  Tween<double> tween = Tween(begin: 0, end: 1);

  _animation = tween.animate(animationController)
    ..addListener(() async {
      final v = _animation!.value;
      double lng = v * _endLng + (1 - v) * _startLng;
      double lat = v * _endLat + (1 - v) * _startLat;
      _markerSink.add(
        Marker(
          markerId: const MarkerId('driverMarker'),
          position: LatLng(lat, lng),
        ),
      );
    });

  animationController.forward();
}

StreamControllerStreamBuilderを使って該当する部分のみリビルドさせるように修正しました。

これでStartボタンを押したらMarkerが動けるようになりました。

ここまで読んでいただき、ありがとうございました!