Flutter で Mapbox の地図を現在位置を中心にして表示してみる

こちらの記事を元に、同じこと（＋α）を Mapbox で試してみました。

 - **[Flutter で Google Map に現在位置を表示する](https://qiita.com/kurararara/items/78709987460024b72b04)（kurararara さん）**

# 準備

まず Mapbox のアカウントを登録し、アクセストークンとシークレットトークンを取得したのち、必要に応じて地図の日本語化を行っておきます。

**【参考】**

 - **[Google Flutter コラム－ 第2 回　GoogleMapとmapboxの地図表示を比較してみた](https://www.nskint.co.jp/2021/06/30/google_flutter_column_2/)（日本システム開発／Google Flutter コラム）**
 - **[mapbox 日本語化の仕方](https://halfpower.work/2020/07/24/mapbox-%E6%97%A5%E6%9C%AC%E8%AA%9E%E5%8C%96%E3%81%AE%E4%BB%95%E6%96%B9/)（半力投球）**

`flutter create 【アプリケーション名】`でアプリケーションを作成し、以下のファイルを編集します。

---

**2021/10/29 追記：**

`mapbox_gl 0.13.0`がリリースされ、Android で日本語の地図が表示できるようになったのと、誤記があったため一部修正しました。

---

※Flutter SDK 2.5.3 にて実行（macOS 11.6）。

### `pubspec.yaml`（関連部分のみ）

```yaml:pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  mapbox_gl: ^0.13.0
  location: ^4.3.0
```

### `android/build.gradle`（Android の場合・関連部分のみ）

```gradle:build.gradle
allprojects {
    repositories {
（中略）
        maven {
            url 'https://api.mapbox.com/downloads/v2/releases/maven'
            authentication {
            basic(BasicAuthentication)
        }
        credentials {
            // Do not change the username below.
            // This should always be `mapbox` (not your username). 
            username = 'mapbox'
            // Use the secret token you stored in gradle.properties as the password
            password = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: ""
            }
        }
    }
}
```

### `android/app/build.gradle`（Android の場合・関連部分のみ）

```gradle:build.gradle
android {
（中略）
    defaultConfig {
（中略）
        minSdkVersion 21
（中略）
    }

    buildTypes {
        release {
            // other configs
            ndk {
                abiFilters 'armeabi-v7a','arm64-v8a','x86_64', 'x86'
            }
        }
    }
```

### `android/app/src/AndroidManifest.xml`（Android の場合・関連部分のみ）

```xml:AndroidManifest.xml
        <meta-data
            android:name="com.mapbox.token"
            android:value="【Mapboxのアクセストークン】"
            />
```

※`<application>`〜`</application>`の間に追記。

### `android/gradle.properties`（Android の場合・関連部分のみ）

```text:gradle.properties
MAPBOX_DOWNLOADS_TOKEN=【Mapboxのシークレットトークン】
```

Android の場合、環境変数`SDK_REGISTRY_TOKEN`にも Mapbox のシークレットトークンをセットしておきます（`flutter run`の際に必要なので、面倒なら`.zshrc`に保存するなどしてください）。

```zsh:
export SDK_REGISTRY_TOKEN="【Mapboxのシークレットトークン】"
```

### `ios/Runner/Info.plist`（iOS の場合・関連部分のみ）

```xml:Info.plist
	<key>NSLocationAlwaysUsageDescription</key>
	<string>Your location is required for this app</string>
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>Your location is required for this app</string>
	<key>MGLMapboxAccessToken</key>
	<string>【Mapboxのアクセストークン】</string>
```

※`<dict>`〜`</dict>`の間に追記。

なお、デバッグ（開発）環境のみ以下の項目も必要になります。デバッグ環境とリリース環境の`Info.plist`の切り替え設定を行った後（例：`Info-Debug.plist`と`Info-Release.plist`に分離して使い分ける）、デバッグ環境用の`Info.plist`のみに以下を追記します。

```xml:Info.plist（デバッグ用）
	<key>NSBonjourServices</key>
	<array>
		<string>_dartobservatory._tcp.</string>
	</array>
```

※`<dict>`〜`</dict>`の間に追記。

### `.netrc`（home ディレクトリへ）

こちらはアプリケーションのディレクトリではなく自分の home ディレクトリ（`/Users/【ユーザ名】`）直下に配置します。

```zsh:.netrc
machine api.mapbox.com
login mapbox
password 【Mapboxのシークレットトークン】
```

ここまで準備できたら、`flutter pub get`します。

# コード例

最初に示した **[kurararara さんの記事](https://qiita.com/kurararara/items/78709987460024b72b04)**のコードをベースに、[`mapbox_gl`](https://pub.dev/packages/mapbox_gl)（執筆時点で 0.12.0）に合わせて書き換えてあります。

また、以下の処理を追加実装しました。

 - GPS の位置情報に合わせて地図を移動（地図の中心を常に現在位置に）
 - 地図上のどこかをタップすると GPS への追従を停止
 - 右下の GPS アイコンボタンのクリックで GPS 追従と停止を切り替え

GPS への追従は[`MyLocationTrackingMode.TrackingGPS`](https://pub.dev/documentation/mapbox_gl_platform_interface/latest/mapbox_gl_platform_interface/MyLocationTrackingMode-class.html)を使うと実装できそうな気がしたのですがうまくいかなかったので、[`location`](https://pub.dev/packages/location)で取得した緯度経度の変化を[`moveCamera`](https://pub.dev/documentation/mapbox_gl/latest/mapbox_gl/MapboxMapController/moveCamera.html)を使って反映しています。

なお、できれば[`animateCamera`](https://pub.dev/documentation/mapbox_gl/latest/mapbox_gl/MapboxMapController/animateCamera.html)を使いたかったのですが、私が所有する Android（10）機ではうまく動きませんでした（iOS 14.8 では正しく動きました）。

```dart:main.dart
import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:location/location.dart';
import 'package:mapbox_gl/mapbox_gl.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Flutter Mapbox',
      home: MapPage(),
    );
  }
}

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

  @override
  _MapPageState createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
  final Completer<MapboxMapController> _controller = Completer();
  final Location _locationService = Location();
  // 地図スタイル用 Mapbox URL
  final String _style = '【スタイルのURL】'; // 地図を日本語化したときなどに必要
  // Location で緯度経度が取れなかったときのデフォルト値
  final double _initialLat = 35.6895014;
  final double _initialLong = 139.6917337;
  // 現在位置
  LocationData? _yourLocation;
  // GPS 追従？
  bool _gpsTracking = false;

  // 現在位置の監視状況
  StreamSubscription? _locationChangedListen;

  @override
  void initState() {
    super.initState();

    // 現在位置の取得
    _getLocation();

    // 現在位置の変化を監視
    _locationChangedListen =
        _locationService.onLocationChanged.listen((LocationData result) async {
      setState(() {
        _yourLocation = result;
      });
    });
    setState(() {
      _gpsTracking = true;
    });
  }

  @override
  void dispose() {
    super.dispose();

    // 監視を終了
    _locationChangedListen?.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _makeMapboxMap(),
      floatingActionButton: _makeGpsIcon(),
    );
  }

  Widget _makeMapboxMap() {
    if (_yourLocation == null) {
      // 現在位置が取れるまではロード中画面を表示
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
    // GPS 追従が ON かつ地図がロードされている→地図の中心を移動
    if (_gpsTracking) {
      _controller.future.then((mapboxMap) {
        mapboxMap.moveCamera(CameraUpdate.newLatLng(LatLng(
            _yourLocation!.latitude ?? _initialLat,
            _yourLocation!.longitude ?? _initialLong)));
      });
    }
    // Mapbox ウィジェットを返す
    return MapboxMap(
      // 地図（スタイル）を指定（デフォルト地図の場合は省略可）
      styleString: _style,
      // 初期表示される位置情報を現在位置から設定
      initialCameraPosition: CameraPosition(
        target: LatLng(_yourLocation!.latitude ?? _initialLat,
            _yourLocation!.longitude ?? _initialLong),
        zoom: 13.5,
      ),
      onMapCreated: (MapboxMapController controller) {
        _controller.complete(controller);
      },
      compassEnabled: true,
      // 現在位置を表示する
      myLocationEnabled: true,
      // 地図をタップしたとき
      onMapClick: (Point<double> point, LatLng tapPoint) {
        _controller.future.then((mapboxMap) {
          mapboxMap.moveCamera(CameraUpdate.newLatLng(tapPoint));
        });
        setState(() {
          _gpsTracking = false;
        });
      },
    );
  }

  Widget _makeGpsIcon() {
    return FloatingActionButton(
      backgroundColor: Colors.blue,
      onPressed: () {
        _gpsToggle();
      },
      child: Icon(
        // GPS 追従の ON / OFF に合わせてアイコン表示する
        _gpsTracking ? Icons.gps_fixed : Icons.gps_not_fixed,
      ),
    );
  }

  void _gpsToggle() {
    setState(() {
      _gpsTracking = !_gpsTracking;
    });
    // ここは iOS では不要
    if (_gpsTracking) {
      _controller.future.then((mapboxMap) {
        mapboxMap.moveCamera(CameraUpdate.newLatLng(LatLng(
            _yourLocation!.latitude ?? _initialLat,
            _yourLocation!.longitude ?? _initialLong)));
      });
    }
  }

  void _getLocation() async {
    _yourLocation = await _locationService.getLocation();
  }
}
```

![IMG_0024.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123899/755f47e0-a706-b914-e427-f591887b3ca8.png)
※デバッグ環境なので、自宅がバレないように（？）GPS 追従を停止した状態の画面です。

# 補足

**【参考】**の**[最初の記事](https://www.nskint.co.jp/2021/06/30/google_flutter_column_2/)**にも書かれていますが、[`mapbox_gl`](https://pub.dev/packages/mapbox_gl)のバージョン 0.12.0 では Android で日本語化した地図の日本語部分の表示ができない問題があります。

---

**2021/10/29 追記：**

前述のとおり、`mapbox_gl 0.13.0`がリリースされ Android で日本語の地図が表示できるようになりました。

---

[`MyLocationRenderMode`](https://pub.dev/documentation/mapbox_gl_platform_interface/latest/mapbox_gl_platform_interface/MyLocationRenderMode-class.html)のように Android は対応しているのに iOS が未対応の機能もありますが、現状、コード例に示した範囲では iOS のほうが機能の対応が進んでいる印象を受けます。

---

**2021/09/24 追記：**
続編を書きました。

 - **[Flutter で Mapbox の地図を操作してみる](https://zenn.dev/hmatsu47/articles/846c3186f5b4fe)**