🗺️

【Flutter】地図パッケージを比較してみた

2022/08/07に公開

Flutter大学内の共同開発プロジェクト9「常夜灯」に参加しているおだけんと申します。
この記事は、「常夜灯」で地図系のパッケージを使用するため、比較し適したパッケージを選定する事を目的としています。

比較対象は、「google_maps_flutter」と「flutter_map」です。
比較基準をいくつかピックアップし、それらにそって比較を行います。

Sample-Codeはsample_google_maps_fluttersample_flutter_mapに公開しています。

目次
  • パッケージ紹介
  • 比較基準
  • 現在地を表示できるか
  • 現在地の表示アイコンを変更できるか
  • 現在向いている方角を表示できるか
  • 任意の場所にピンを立てることができるか
  • ピンのアイコンは変更可能か
  • ピンの大きさは変更可能か
  • 地図そのものの見た目を柔軟に変更できるか
  • 料金がどのくらいかかるか
  • ピンを立てることや地図利用そのものに料金は発生するか
  • パッケージのAPIは使いやすいか
  • まとめ

パッケージ紹介

google_maps_flutter

https://pub.dev/packages/google_maps_flutter


flutter_map

https://pub.dev/packages/flutter_map


比較基準

  • 現在地を表示できるか
  • 現在地の表示アイコンを変更できるか
  • 現在向いている方角を表示できるか
  • 任意の場所にピンを立てることができるか
  • ピンのアイコンは変更可能か
  • ピンの大きさは変更可能か
  • 地図そのものの見た目を柔軟に変更できるか
  • 料金がどのくらいかかるか
  • ピンを立てることや地図利用そのものに料金は発生するか
  • パッケージのAPIは使いやすいか

現在地を表示できるか

・google_maps_flutter

現在地の表示は可能です。
このパッケージでは、GoogleMapクラス内にmyLocationEnabledというフィールドを持っているため、これをtrueにすると現在地の表示が可能になります。

body: GoogleMap(
    myLocationEnabled: true,
)

・flutter_map

このパッケージでは、現在地を表示する機能は備わっていないため、表示ができません。
表示したい場合は別のGeolocation系パッケージと併用していく必要があります。
下記に使用例を記載します。今回はGeolocationには「geolocator」パッケージを使用します。
また、緯度経度の取得の際にLatLng2パッケージの使用します。
https://pub.dev/packages/geolocator

https://pub.dev/packages/latlong2/install

import 'package:latlong2/latlong.dart';
import 'package:geolocator/geolocator.dart';

class MapApp extends StatefulWidget {
  
  _MapAppState createState() => _MapAppState();
}

class _MapAppState extends State<MapApp> {
  String _title = 'Sample flutter_map';

  List<CircleMarker> circleMarkers = [];

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'map_app',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text(_title),
        ),
        // flutter_map設定
        body: FlutterMap(
          // マップ表示設定
          options: MapOptions(
            center: LatLng(35.681, 139.767),
            zoom: 14.0,
            interactiveFlags: InteractiveFlag.all,
            enableScrollWheel: true,
            scrollWheelVelocity: 0.00001,
          ),
          layers: [
            // 背景地図読み込み (OSM)
            TileLayerOptions(
              urlTemplate: "https://tile.openstreetmap.jp/{z}/{x}/{y}.png",
            ), // OpenStreetMap-APIを使用
            // サークルマーカー設定
            CircleLayerOptions(
              circles: circleMarkers,
            ),
          ],
        ),
      ),
    );
  }

  
  void initState() {
    super.initState();
    initLocation();
  }

  Future<void> initLocation() async {
    LocationPermission permission = await Geolocator.checkPermission();
    if (permission == LocationPermission.denied) {
      permission = await Geolocator.requestPermission();
      if (permission == LocationPermission.denied) {
        return Future.error('Location permissions are denied');
      }
    }
    Position position = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);
    final latitude = position.latitude;
    final longitude = position.longitude;
    initCircleMarker(latitude, longitude);
    setState(() {});
  }

  void initCircleMarker(double latitude, double longitude) {
    CircleMarker circleMarler = CircleMarker(
      color: Colors.indigo.withOpacity(0.9),
      radius: 10,
      borderColor: Colors.white.withOpacity(0.9),
      borderStrokeWidth: 3,
      point: LatLng(latitude, longitude),
    );
    circleMarkers.add(circleMarler);
  }
}

現在地の表示アイコンを変更できるか

・google_maps_flutter

標準では現在地のアイコンは変更ができません。
備わっているマーカーとは別に自作しマーカーを上書き配置する場合は、アイコンの変更が可能です。

・flutter_map

表示アイコンの変更は可能です。
Geolocation系パッケージと併用していくやり方では、自作でマーカーを作成しているのでマーカーのIconを任意のものへ変更することで実現が可能です。下記に例を記載します。

  List<Marker> markers = [
    Marker(
      point: LatLng(35.697, 139.775),
      width: 80,
      height: 80,
      builder: (context) => Container(
        child: IconButton(
          icon: Icon(
            Icons.location_on, //←任意のIconへ変更可能
            size: 40,
            color: Colors.red,
          ),
          onPressed: () {},
        ),
      ),
    )
  ];

現在向いている方角を表示できるか

・google_maps_flutter

向いている方角の取得は可能です。
「現在地を表示できるか」の題目でもあったmyLocationEnabled: trueの設定を加えることで向いている方角を取得し追尾してくれます。

・flutter_map

標準では向いている方角の取得はできません。
こちらもGeolocation系パッケージと併用していく必要があります。
「geolocator」パッケージにはgetCurrentPositionメソッドが用意されているので、そこで向いている方角(上方向)を取得することができます。

Position position = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high);
    currentHeading = position.heading;

任意の場所にピンを立てることができるか

・google_maps_flutter

ピンを立てることは可能です。
緯度経度がわかればピンとしてマップ上に記載ができます。
GoogleMapクラス内にonTapフィールドが用意されているため、今回はonTapをトリガーにマーカーを作成します。
onTapではLatLng型がコールバックされるため、緯度経度の取得が可能です。
今回はCanvasを使用してマーカーをピンとして生成します。

body: GoogleMap(
        onTap: (LatLng latLng) async {
          ontapLatLng = latLng;
          await markerCreate(latLng);
        },
        markers: _markers,
      ),

  // CanvasMarkerを作成する関数
  Future<void> markerCreate(LatLng latLng) async {
    // 初期のCanvasサイズを指定
    final Uint8List markerIcon = await getBytesFromCanvas(50, 50);

    Marker initMarker = Marker(
      markerId: MarkerId(latLng.toString()), // IDにはlatLngを使用する
      icon: BitmapDescriptor.fromBytes(markerIcon),
      position: latLng, // 配置する緯度経度を設定
    );

    setState(() {
      markerAdd(initMarker);
    });
  }

// 引数からUint8List型でCancvasをリターンする関数
Future<Uint8List> getBytesFromCanvas(int width, int height) async {
  final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
  final Canvas canvas = Canvas(pictureRecorder);
  Color yellowcolors = colorReturn(width);
  final Paint paint = Paint()..color = yellowcolors;
  final Radius radius = Radius.circular(30.0);

  canvas.drawRRect(
      RRect.fromRectAndCorners(
        Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()),
        topLeft: radius,
        topRight: radius,
        bottomLeft: radius,
        bottomRight: radius,
      ),
      paint);

  TextPainter Painter = TextPainter(textDirection: TextDirection.ltr);
  Painter.text = TextSpan(
    text: 'H', // Canvas内に表示するText
    style: TextStyle(fontSize: 25.0, color: Colors.white),
  );
  Painter.layout();
  Painter.paint(
      canvas,
      Offset((width * 0.5) - Painter.width * 0.5,
          (height * 0.5) - Painter.height * 0.5));
  final img = await pictureRecorder.endRecording().toImage(width, height);
  final data = await img.toByteData(format: ui.ImageByteFormat.png);
  return data!.buffer.asUint8List();
}

・flutter_map

ピンを立てることは可能です。
こちらも同様に緯度経度がわかればピンとしてマップ上に記載ができます。
「flutter_map」でもonTapが用意されているためこれをトリガーにピンを作成できます。

List<Marker> markers = [];

body: FlutterMap(
  // マップ表示設定
  options: MapOptions(
      onTap: (dynamic tapPosition,LatLng latLng) {
        tapLatLng = latLng;
        cleatePin(tapLatLng!);
      }),
    // ピンマーカー設定
    MarkerLayerOptions(
      markers: markers,
    ),
  ),
	
void cleatePin(LatLng tapLatLng) {
    Marker marker = Marker(
      point: tapLatLng,
      width: 80,
      height: 80,
      builder: (context) => Container(
        child: IconButton(
          icon: Icon(
            Icons.location_on,
            size: 40,
            color: Colors.red,
          ),
        ),
      ),
    );
    markers.add(marker);
    setState(() {});
  }

ピンのアイコンは変更可能か

・google_maps_flutter

アイコンの変更は可能です。
マップ上にマーカーを生成する方法はいくつかあり、Assetを使用する方法とCanvasを使用してマーカーを生成する方法があります。
GoogleMap内のmarkersフィールドへMarkerを渡してあげると生成可能です。

 Assetから生成  Canvasから生成
Assetからの生成
import 'dart:ui' as ui;
import 'dart:typed_data';
import 'package:flutter/services.dart';

Future<void> markerCreate(LatLng latLng) async {
        // 初期のAssetから生成されるIconサイズを指定
    int iconsize = 100;
    final Uint8List markerIcon =
        await getBytesFromAsset('自身の画像パスを入力.png', iconsize);

    Marker initMarker = Marker(
      markerId: MarkerId(latLng.toString()), // IDにはlatLngを使用する
      icon: BitmapDescriptor.fromBytes(markerIcon),
      position: latLng,
    );

    setState(() {
      _markers.add(initMarker);
    });
  }

  Future<Uint8List> getBytesFromAsset(String path, int width) async {
    ByteData data = await rootBundle.load(path);
    ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),
        targetWidth: width);
    ui.FrameInfo fi = await codec.getNextFrame();
    return (await fi.image.toByteData(format: ui.ImageByteFormat.png))!
        .buffer
        .asUint8List();
  }
Canvasからの生成
  Future<void> markerCreate(LatLng latLng) async {
    // 初期のCanvasサイズを指定
        int width = 50;
    int height = 50final Uint8List markerIcon = await getBytesFromCanvas(width, height);

    Marker initMarker = Marker(
      markerId: MarkerId(latLng.toString()), // IDにはlatLngを使用する
      icon: BitmapDescriptor.fromBytes(markerIcon),
      position: latLng,
    );

    setState(() {
      _markers.add(initMarker);
    });
  }

Future<Uint8List> getBytesFromCanvas(int width, int height) async {
  final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
  final Canvas canvas = Canvas(pictureRecorder);
  Color yellowcolors = colorReturn(width);
  final Paint paint = Paint()..color = Colors.red;
  final Radius radius = Radius.circular(30.0);

  canvas.drawRRect(
      RRect.fromRectAndCorners(
        Rect.fromLTWH(0.0, 0.0, width.toDouble(), height.toDouble()),
        topLeft: radius,
        topRight: radius,
        bottomLeft: radius,
        bottomRight: radius,
      ),
      paint);

  TextPainter Painter = TextPainter(textDirection: TextDirection.ltr);
  Painter.text = TextSpan(
    text: 'H',
    style: TextStyle(fontSize: 25.0, color: Colors.white),
  );
  Painter.layout();
  Painter.paint(
      canvas,
      Offset((width * 0.5) - Painter.width * 0.5,
          (height * 0.5) - Painter.height * 0.5));
  final img = await pictureRecorder.endRecording().toImage(width, height);
  final data = await img.toByteData(format: ui.ImageByteFormat.png);
  return data!.buffer.asUint8List();
}

・flutter_map

flutter_mapはマーカーにIconButtonを使用しているため、任意のIconへ変更可能です。

List<Marker> markers = [
    Marker(
      point: LatLng(35.697, 139.775),
      width: 80,
      height: 80,
      builder: (context) => Container(
        child: IconButton(
          icon: Icon(
            Icons.home, //←任意のIconへ変更可能
            size: 40,
            color: Colors.blue,
          ),
          onPressed: () {},
        ),
      ),
    )
  ];

地図そのものの見た目を柔軟に変更できるか

・google_maps_flutter

地図の見た目は変更可能です。
「google_maps_flutter」では、GoogleCloudPlatformが提供するGoogleMapsPlatformのAPIを使用します。
GoogleMapsPlatformではMap Stylesを自身で作成してJson形式で出力ができます。

Jsonファイルの出力方法

Jsonの出力方法は下記のStyling Wizardにアクセスし、Old Style Wizardを選択します。
https://mapstyle.withgoogle.com/

自身に合ったカスタムMAPを作成します。
その後、左下の「FINISH」を選択します。

JsonコードのExportが可能になるのでコピーします。

そのJsonをMapに適用することで地図を変更することができます。
今回はmapstyle.jsonという名前でファイルを生成しています。

body: GoogleMap(
  onMapCreated: (GoogleMapController controller) {
    _onMapCreated(controller);
  },
),

// マップの初期構築
Future<void> _onMapCreated(GoogleMapController controller) async {
  _controller.complete(controller);
  String value = await DefaultAssetBundle.of(context)
      .loadString('lib/json/mapstyle.json'); //カスタムしたMAPのJsonファイルをを読み込む
  GoogleMapController futureController = await _controller.future;
  futureController.setMapStyle(value); // Controllerを使ってMapをSetする
}

・flutter_map

パッケージ内では地図の見た目を変更できません。
別途設定を行うと見た目の変更が可能です。
「flutter_map」ではOpenStreetMap(OSM)、BingMap、MaptilerなどのAPIを使用します。そのためそれぞれのプラットフォームで設定を行うことで実現できます。
https://www.openstreetmap.org/#map=6/33.688/131.990
https://www.microsoft.com/en-us/maps/choose-your-bing-maps-api
https://maptiler.jp/jp/


料金がどのくらいかかるか

・google_maps_flutter

GoogleMapsPlatformは 1 か月 200 ドル分まで無料で利用できます。
地図の読み込みは 1 か月あたり 28,500 回まで無料です。
GoogleMapsPlatformは従量課金制を採用しており、
その範囲を超える場合は使用した分だけ料金が発生します。
インスタンス化されるたびに、地図の読み込みが 1 回発生します。
自身に合った料金を調査した場合は下記を参照ください。
https://mapsplatform.google.com/intl/ja/pricing/

・flutter_map

オープンソースのMAP-APIを使用する場合は、料金は発生しません。
無料のMAP-APIが利用できる代表例としては、「OpenStreetMap(OSM)」「電子国土web」などがあります。各プラットフォームでのドキュメントを参照して確認ください。
https://wiki.openstreetmap.org/wiki/JA:Main_Page
http://www.openspc2.org/reibun/kokudo/index.html


ピンを立てることや地図利用そのものに料金は発生するか

・google_maps_flutter

ピンを立てることには発生しません。
地図料金はGoogleMapsPlatformの利用で料金が発生します。
地図そのものは背景としてアプリ内に配置し、ピンはその上に重ねる構造になっています。

・flutter_map

上記と同様にピンを立てたり、地図利用には料金は発生しません。


パッケージのAPIは使いやすいか

・google_maps_flutter

パッケージを導入するとIOS/Androidの異なるプラットフォーム上でMAPが生成されるためとても便利です。
また、標準で備わっている機能が多く画面上に表示できるMAPの形式を柔軟に変更可能なため、とてもよいです。
不便な点は、現在地マーカーの見た目を容易に変更できないことなどが挙げられます。

・flutter_map

パッケージで使用するMAP-APIを柔軟に変更ができるため料金をかけたくない方や、使用したいAPIがある方には重宝すると思います。
また、マーカー作成時には、Flutterで使用されるIconを使うことができるため、操作が容易です。


まとめ

パッケージ 使い易さ 機能性 オフライン対応 料金 MAP-API
google_maps_flutter ✖︎ △ ※1 Google Maps Platform
flutter_map △ ※2 OSM/Bing Maps/MapTilerなど

※1 無料枠を超えた場合は料金が発生します。
※2 MAP-APIを有料のものを使用した場合は、別途料金が発生する場合があります。


参考文献

https://pub.dev/packages/google_maps_flutter
https://pub.dev/packages/google_maps_flutter

https://pub.dev/packages/flutter_map
https://pub.dev/packages/flutter_map

Discussion