flutter_mapでMapbox Maps SDK Flutter Pluginを利用する
はじめに
flutter_mapはFlutterでマップアプリケーションを実装する上でお馴染みのツールで利用したことのある方も多いと思います。google_maps_flutterと同じくらい有名なのではないでしょうか。
flutter_mapがgoogle_maps_flutterよりも優れている点の一つに、タイルレイヤーを自由に設定できるという点があります。弊社のアプリケーションでもこの点でflutter_mapを採用し、タイルレイヤーにMapboxのStatic Tile APIを採用しで独自でデザインしたマップを表示するようにしていました。しかし、このStatic Tile APIがシード期のスタートアップには比較的高額でコストを圧迫するという問題に直面しました。
技術選定
この問題を回避する方法として、flutter_mapの代わりにモバイル用に提供されているSDKを利用する方法があります。flutterでこのSDKを利用できるパッケージは下記の二つです
古くからあるmapbox_glは最近メンテナンスがされていないため、今回の候補からは外しました。もう一つのmapbox_maps_flutterはMapboxが公式で提供しているパッケージのためこちらを利用するのが最も望ましいと考えられます。しかし、このパッケージは出たばかりでまだ全ての機能が利用できず、特にView Annotationsが利用できないのは要件を満たせないためそのまま採用することができませんでした。
解決策と実装
そこで色々調査した結果下記のようなdiscussionsを発見しました!google_tile_layer_widget.dartがドンピシャでやりたいことを実現できるだけではなく、元々実装していたflutter_mapのコードをほとんど変えずにMapboxのSDKを利用することができました。(もう本当感謝です泣)
上記コードはGoogle Mapを利用するための実装のため、弊社で実際に利用しているMapbox用のコードを下記に示します。(Mapboxに変えると若干位置ずれが発生するためパラメータは少しいじってます。)
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'
show MapCamera, PointExtension, TileLayer;
import 'package:latlong2/latlong.dart';
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart';
const double kMaxZoom = 21;
const double kMinZoom = 3;
class MapboxTileLayer extends StatefulWidget {
const MapboxTileLayer({
super.key,
this.padding = EdgeInsets.zero,
this.trafficEnabled = false,
this.buildingsEnabled = false,
this.compassEnabled = false,
this.myLocationEnabled = false,
this.indoorViewEnabled = false,
this.layoutDirection,
});
final EdgeInsets padding;
final bool trafficEnabled;
final bool buildingsEnabled;
final bool compassEnabled;
final bool myLocationEnabled;
final bool indoorViewEnabled;
final TextDirection? layoutDirection;
@override
State<MapboxTileLayer> createState() => _MapboxTileLayerState();
}
class _MapboxTileLayerState extends State<MapboxTileLayer> {
MapboxMap? _controller;
final String _mapboxAPIKey = const String.fromEnvironment('MAPBOX_API_KEY');
Future<void> mapEvent(MapCamera mapCamera) async {
if (_controller != null) {
final center = convertLatLng(mapCamera);
final zoom = mapCamera.zoom;
final rotation = mapCamera.rotation;
try {
await _controller!.setCamera(
CameraOptions(
bearing: -rotation,
center: Point(
coordinates: Position(center.longitude, center.latitude),
).toJson(),
zoom: zoom - 1,
),
);
} catch (e) {
debugPrint(e.toString());
}
}
}
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
// Mapbox APIキーが設定されてなかったらOpenStreetMapを利用
if (_mapboxAPIKey == '') {
return TileLayer(
urlTemplate:
'https://tile.openstreetmap.jp/styles/osm-bright-ja/{z}/{x}/{y}.png',
);
}
final mapCamera = MapCamera.of(context);
mapEvent(mapCamera);
return Stack(
alignment: Alignment.center,
children: [
IgnorePointer(
child: MapWidget(
styleUri: '<利用したいStyleUriを設定>',
key: const ValueKey('mapWidget'),
resourceOptions: ResourceOptions(accessToken: _mapboxAPIKey),
cameraOptions: CameraOptions(
center: Point(
coordinates: Position(
convertLatLng(mapCamera).longitude,
convertLatLng(mapCamera).latitude,
),
).toJson(),
zoom: 14,
),
onMapCreated: (MapboxMap mapboxMap) {
_controller = mapboxMap;
WidgetsBinding.instance.addPostFrameCallback((_) {
mapEvent(mapCamera);
setState(() {});
});
},
),
),
Container(
color: Colors.transparent,
),
],
);
}
LatLng convertLatLng(MapCamera mapCamera) {
final center = mapCamera.center;
final zoom = mapCamera.zoom;
final padding = widget.padding;
final top = padding.top;
final right = padding.right;
final bottom = padding.bottom;
final left = padding.left;
final mapCenterPoint = mapCamera.project(center, zoom);
final adjustedCenterPoint = math.Point(
mapCenterPoint.x + left / 2 - right / 2,
mapCenterPoint.y + top / 2 - bottom / 2,
);
final rotatePoint =
mapCamera.rotatePoint(mapCenterPoint, adjustedCenterPoint);
final rounded = rotatePoint.round(); // optional
final fixedLatLng = mapCamera.unproject(rounded, zoom);
return LatLng(fixedLatLng.latitude, fixedLatLng.longitude);
}
}
下記のように利用できます。
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return FlutterMap(
options: MapOptions(
maxZoom: kMaxZoom,
minZoom: kMinZoom,
initialCenter: LatLng(15.5, 30),
initialZoom: 5,
),
children: [
MapboxTileLayer(),
],
);
}
}
最後に
mapbox_maps_flutterは公式が提供しているパッケージのため今後本記事のような実装を利用しなくても実現したいことは叶うようになるかなとも思いますが、今すぐ実装で利用したいと思っている方もいるのではないでしょうか。
そういった方の助けに少しでもなれれば幸いです。
Discussion