FlutterでState管理されている緯度経度に向かってGoogleMapController#animateCamera する

## まえおき

GoogleMapをFlutterで使うときは、 https://pub.dev/packages/google_maps_flutter を使うことになると思う。

[一つ前の記事](https://qiita.com/YusukeIwaki/items/d4979fc698d7f0998e30) で、

![image](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F74571%2Fc9483d82-08f4-5d63-0e22-79b984bda514.gif?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&w=1400&fit=max&s=eca28d19b0b0fbeab67a6d82e19704c8)

こんなUIを作っている話をしていたが、これをFlutterに移植しようとしたときにハマったのでメモ。

## やりたかったこと

ページをスワイプして変更したときには、地図側で

* 選択されたお店の緯度経度に向かってanimateCameraする
* 選択されたお店のマーカーアイコンを切り替える

ということをやりたい。普通に考えれば、「選択されたお店」をStateにもたせてしまって、それをGoogleMapとPageViewに反映させる作りがよさそう。

![Slice.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/ea3508b4-ccb8-391b-f988-fb120337b332.png)

### でもぐぐって出てくるのは・・・

サンプルコードでも、ググって出てくる大抵の例も、

![Slice.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/f42df2ff-24c1-0613-7e09-feabb8cd0668.png)

こんな感じで、イベントハンドラから直接 googleMapController.animateCamera している（状態は書き換えていない）のが多い。
状態が変化して、ウィジェットを再レイアウトする際にanimateCameraしている例が全然見つからなかった。

## うまくいったやり方

結論から書こう。2019/10月現在は以下のやりかたでうまく行った。

```dart
class PlaceListPageState extends State {
  static CameraPosition _initCameraPosition = ....
  Completer<GoogleMapController> _googleMapController = Completer();

  List<Place> places = List();
  Place selectedPlace = null; // 地図で中心表示にしたい＆PageViewで選択したい場所

  @override
  Widget build(BuildContext context) {
    // マップがロード済みであれば、
    _googleMapController.future.then((googleMap) {
      // 中心に表示したい場所があれば、そこに向かってanimateCameraする。
      if (selectedPlace != null) {
        googleMap.animateCamera(CameraUpdate.newLatLng(selectedPlace.location));
      }
    });

    return Column(
      children: <Widget>[

          (中略)

          child: GoogleMap(
            initialCameraPosition: _initCameraPosition,
            markers: places.map((place) {
              return Marker(
                  markerId: MarkerId(place.uuid),
                  position: place.location,
                  icon: place.uuid == selectedPlace?.uuid ? 選択時のアイコン : デフォルト時のアイコン,
                  onTap: () {
                    // マーカーをタップしたら中心の場所を更新する
                    setState(() { selectedPlace = place; });
                  });
            }).toSet(),
            onMapCreated: (googleMap) {
              _googleMapController.complete(googleMap);
            },
          ),
        ),

　        (中略)

          child: PageView(
            children: ....,
            onPageChanged: (page) {
              // PaveViewのページ変更契機でも中心の場所を更新する
              setState(() { selectedPlace = places.elementAt(page); })
            },
          ),

```

`build()` で愚直に、Completerにつつんだ GoogleMapController の animateCamera を呼ぶだけ、であった。

## うまくいかなかった方法

### initialCameraPositionをselectedPlaceの有無によって変える

```dart
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[

          (中略)

        child: GoogleMap(
          initialCameraPosition: selectedPlace?.locationAsCameraPosition() ?? _initCameraPosition,
          markers: places.map((place) {

```

こういうやり方。

Stack Overflowで、 [これ](https://stackoverflow.com/questions/56416528/flutter-google-maps-not-determining-current-location-of-device) とか [これ](https://stackoverflow.com/questions/51755798/flutter-map-view-center-to-user-location) とか見ると、initialCameraPositionをリアクティブにマップで表示してくれそうに見えるが、実際にはそんなことはない。

理由はGoogleMapのライブラリのソースを見れば一目瞭然で、

```google_map.dart
  Future<void> onPlatformViewCreated(int id) async {
    final GoogleMapController controller = await GoogleMapController.init(
      id,
      widget.initialCameraPosition,
      this,
    );
    _controller.complete(controller);
    if (widget.onMapCreated != null) {
      widget.onMapCreated(controller);
    }
  }
```

GoogleMapできあがったよイベント（ネイティブからのコールバック）で**最初の１回だけしかinitialCameraPositionは評価されない**。State更新時にはinitialCameraPositionを見ていないのだ。

ちなみに蛇足だが、マーカーとか図形とかを描画してるのは、以下のように更新されるらしい。

```google_map.dart
  @override
  void didUpdateWidget(GoogleMap oldWidget) {
    super.didUpdateWidget(oldWidget);
    _updateOptions();
    _updateMarkers();
    _updatePolygons();
    _updatePolylines();
    _updateCircles();
  }

  void _updateOptions() async {
    final _GoogleMapOptions newOptions = _GoogleMapOptions.fromWidget(widget);
    final Map<String, dynamic> updates =
        _googleMapOptions.updatesMap(newOptions);
    if (updates.isEmpty) {
      return;
    }
    final GoogleMapController controller = await _controller.future;
    controller._updateMapOptions(updates);
    _googleMapOptions = newOptions;
  }

  void _updateMarkers() async {
    final GoogleMapController controller = await _controller.future;
    controller._updateMarkers(
        _MarkerUpdates.from(_markers.values.toSet(), widget.markers));
    _markers = _keyByMarkerId(widget.markers);
  }

  void _updatePolygons() async {
    final GoogleMapController controller = await _controller.future;
    controller._updatePolygons(
        _PolygonUpdates.from(_polygons.values.toSet(), widget.polygons));
    _polygons = _keyByPolygonId(widget.polygons);
  }

  void _updatePolylines() async {
    final GoogleMapController controller = await _controller.future;
    controller._updatePolylines(
        _PolylineUpdates.from(_polylines.values.toSet(), widget.polylines));
    _polylines = _keyByPolylineId(widget.polylines);
  }

  void _updateCircles() async {
    final GoogleMapController controller = await _controller.future;
    controller._updateCircles(
        _CircleUpdates.from(_circles.values.toSet(), widget.circles));
    _circles = _keyByCircleId(widget.circles);
  }
```

## まとめ

* GoogleMapの onMapCreated はネイティブのGoogleMapが作られたときに１回だけ呼ばれる。
* GoogleMapのinitialCameraPositionはonMapCreatedの直前で１回だけ評価される。
* よってState管理している緯度経度にanimateCameraしたければ、 `build()` で愚直にそういう処理を書く必要がある。