完全に自分向けのメモです。あまり人に読ませるように書いてないのはご了承ください。
勾配ワカールというアプリをFlutterで作成したのですが、その際に得た知見と遭遇したトラブルを晒しておきます。
目次
使用環境
Flutter 2.10.5
地図の再描画処理が走ったところでjava.lang.IllegalStateException: Calling addLayer when a newer style is loading/has loaded.が発生した(Android版)
勾配ワカールでは地図描画にMapboxの地図を使用しています。ライブラリは以下を使用しています。
mapbox_gl: ^0.16.0
ルート検索中に画面いっぱいにCircularProgressbarを表示し、ルート検索完了後にCIrcularProgressbarを消して地図上にルートを描画する処理にしていたのですが、この際に以下の例外が発生していました。
E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): Failed to handle method call E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): java.lang.IllegalStateException: Calling addLayer when a newer style is loading/has loaded. E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at com.mapbox.mapboxsdk.maps.Style.validateState(Style.java:786) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at com.mapbox.mapboxsdk.maps.Style.addLayer(Style.java:190) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at com.mapbox.mapboxgl.MapboxMapController.addLineLayer(MapboxMapController.java:447) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at com.mapbox.mapboxgl.MapboxMapController.onMethodCall(MapboxMapController.java:882) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at io.flutter.plugin.common.MethodChannel$IncomingMethodCallHandler.onMessage(MethodChannel.java:262) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at io.flutter.embedding.engine.dart.DartMessenger.invokeHandler(DartMessenger.java:296) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at io.flutter.embedding.engine.dart.DartMessenger.lambda$dispatchMessageToQueue$0$DartMessenger(DartMessenger.java:320) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at io.flutter.embedding.engine.dart.-$$Lambda$DartMessenger$TsixYUB5E6FpKhMtCSQVHKE89gQ.run(Unknown Source:12) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at android.os.Handler.handleCallback(Handler.java:938) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at android.os.Handler.dispatchMessage(Handler.java:99) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at android.os.Looper.loop(Looper.java:263) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at android.app.ActivityThread.main(ActivityThread.java:8326) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at java.lang.reflect.Method.invoke(Native Method) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612) E/MethodChannel#plugins.flutter.io/mapbox_maps_2(19119): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1006)
Calling addLayer when a newer style is loading/has loaded.と表示されていたので、Mapboxに追加したレイヤーに何か問題があるのかな?と思いました。
mapbox_glでは、地図上に線を引く際に、線を描画するためのデータとレイヤーを追加する必要があります。追加処理はこのようにしていました。
var geometry = {"coordinates": toJSON2(routes), "type": "LineString"}; // 実際に線を引くための座標データ final _fills = { "type": "FeatureCollection", "features": [ { "type": "Feature", "id": 0, "properties": <String, dynamic>{}, "geometry": geometry, }, ] }; await _mapController?.addSource( "fillsSource", mapbox.GeojsonSourceProperties(data: _fills)); await _mapController?.addLineLayer( "fillsSource", // 地図上に描画する線のデータの名前 "fillsLine2", // レイヤの名前 mapbox.LineLayerProperties( lineColor: Colors.black.toHexStringRGB(), lineCap: "round", lineJoin: "round", lineWidth: 3));
エラーメッセージからして追加処理に問題があるのかな?と思ったのですが、全然違うところに原因がありました。
return ModalProgressHUD( inAsyncCall: state.fetching, child: LayoutBuilder( builder: (_, constraints) => Stack( alignment: Alignment.center, children: [ MapboxMap( styleString: "mapbox://****************", accessToken: apiKeyMapbox, // 地図(スタイル)を指定(デフォルト地図の場合は省略可) // styleString: _style, // 初期表示される位置情報を現在位置から設定 initialCameraPosition: initialCameraPosition, onMapCreated: (MapboxMapController mapController) { WidgetsBinding.instance!.addPostFrameCallback((_) { controller.onMapCreated(mapController); }); }, compassEnabled: true, // 現在位置を表示する myLocationEnabled: true, onCameraIdle: controller.onCameraIdle, trackCameraPosition: true, ), ], ), ), );
ModalProgressHUDというウィジェットは、isAsyncCall引数がtrueの時に、childに指定したwidgetに重ねる形でCircularProgressbarを表示させるためのプラグインです。使用しているプラグインはこちらです。
modal_progress_hud_nsn: ^0.3.0
ルート検索中は、state.fetchingがtrueに変わるのですが、それをきっかけにModalProgressHUDが動き出してCircularProgressbarが全面表示されます。
そしてルート検索が終わってstate.fetchingがfalseに変わった時に、MapboxMapのonMapCreated引数が再度実行されました。
原因はここにありました。onMapCreated引数が再度実行されたことにより、MapboxMapControllerが再度生成されてしまいました。
このことが原因でMapboxMapControllerの状態がおかしくなり、上記例外が発生していました。
ModalProgressHUDを取り外したことによりこの例外は消えました。
これは難しかった。。。。
APIキーといった機微な情報をソースコード上に書かない方法
Mapboxの地図を利用するには、あらかじめmapbox.comでアカウントを作成し、APIキーを払い出しておく必要があります。
Mapboxに限らず、当該サービスを利用するためにAPIキーを払い出すことはとてもよくあるシチュエーションですが、このAPIキーが漏れてしまうと悪意のある人に予期せぬサービス利用を許してしまい、課金させられてしまう事態になりかねません。
ではどのようにAPIキーをもたせてやったらいいでしょうか。
結論:ビルド時にdart-defineオプションで値を引き渡す
flutter buid コマンドに --dart-define というオプションがあるのでこれ経由でAPIキーを渡します
iOS向けビルド
flutter build ipa --release --export-options-plist=ios/ExportOptions.plist --dart-define=MAPBOX_API_KEY=(APIキーの値)
Android向けビルド
flutter build appbundle --release --dart-define=MAPBOX_API_KEY=(APIキーの値)
では、Flutterのコード上などではどのようにその値を取得したらいいでしょうか。
このように書きます。
const apiKeyMapbox = String.fromEnvironment('MAPBOX_API_KEY');
String.fromEnvironmentメソッドを使うことでdart-defineで指定した値を取得することができます。
ちなみに、android/app/build.gradleで取得することも可能です。
// dart-defineを取得する def dartDefines= [:]; if (project.hasProperty('dart-defines')) { dartDefines = project.property('dart-defines').split(',').collectEntries{ entry -> def pair = new String(entry.decodeBase64(), 'UTF-8').split('=') [(pair.first()): pair.last()] } }
ただ実際には私はこれは使いませんでした。どちらかといえばandroid/build.gradleでdart-defineの値を取得したかったのですが、android/build.gradleではうまく動きませんでした。android/build.gradleでdart-defineの値を取得する方法をご存知の方がいらっしゃったら教えて欲しいです。
参考URL
Android Studioのコード自動補完機能が効かなくなった場合の対処方法
使用していたバージョン
Android Studio Dolphin | 2021.3.1 Patch 1
対処方法
pubspecs.yamlにプラグインを記載してpub getした直後だと、なぜかインストールしたプラグインが提供しているクラスを使用してもimport文をお勧めしてきてくれないことがあります。
そういうときは、下部に表示されている[Dart Analysis]を選択し、左上に表示されている赤い更新ボタンを一回クリックすると、しばらくして自動補完が機能するようになり、import文の追記をお勧めしてくれるようになります。
GoogleMapでは「マーカー」にあたるものは、Mapboxでは"Symbol"
MapboxではSymbolというものを地図上にピンとしておくことができます。
地図サービスによって呼び名がちょっとずつ違うのは面倒ですね。
Widget build(BuildContext context) { mapController.addSymbol( SymbolOptions( geometry: LatLng(-33.86711, 151.1947171), iconImage: "assets/images/ur_image.png", ), ); return Scaffold( body: Center( child: SizedBox( width: double.infinity, height: 300.0, child: MapboxMap( styleString: themeControl.themeSet.mapStyle, onMapCreated: _onMapCreated, onStyleLoadedCallback: _onStyleLoaded, zoomGesturesEnabled: _zoomGesturesEnabled, myLocationEnabled: _myLocationEnabled, initialCameraPosition: const CameraPosition( target: LatLng(-33.852, 151.211), zoom: 11.0, ), ), ), ), );}
参考URL
Symbolに使う画像はアプリ内で用意するのではなくてMapboxのDashboardで先にアップロードしておく
Google Mapだと、アプリ内のコードでカスタムマーカーの画像などを用意しますが、MapboxではあらかじめMapboxのDashboardでその画像をアップロードしておく必要があります。
参考URL
freezed
要するに状態を持たせる変数を総括的に管理するパッケージだと思って。
FlutterCampus Flutterで作ったアプリのデザイン集
アプリのデザインに困ったらここを見るといろんなデザインがあるので刺激を受けます。
アプリアイコンとして使用させていただいたもの
こちらのサイトから使用させていただきました。
勾配ワカールはこちらからダウンロード
iOS版
Android版