FlutterとNCMBで地図メモアプリを作る（その3: マーカー表示と一覧表示）

スマートフォンと地図は相性が良いです。スマホは持ち歩いて使うのが基本ですし、位置情報などの情報も取得できます。

今回はNCMBとFlutterを使って地図上にメモできる地図メモアプリを作ります。地図はOpenStreetMapのものを利用し、タップした場所にメモと写真を残しておけるアプリです。

[前回は地図表示とデータの登録を実装](https://qiita.com/goofmint/items/38b1fc9001de1a4b0e56)しましたので、今回は地図上へのマーカー表示と一覧表示処理を実装していきます。

## コードについて

今回のコードは [flutter_map_note](https://github.com/NCMBMania/flutter_map_note) にアップロードしてあります。実装時の参考にしてください。

## 地図上へのマーカー表示

この処理は MapPage にて実装します。地図の初期化タイミングでは `transfer` がないため、中心点を移動し終わったタイミングで処理を行っています。

```dart
void _onScaleEnd(MapTransformer transformer) {
  // マーカーを表示する
  showNotes(transformer);
}

Future<void> showNotes(MapTransformer transformer) async {
  final notes = await getNotes(transformer.controller.center);
  final markers = await Future.wait(notes.map((note) async {
    return await _buildMarkerWidget(note, transformer);
  }));
  setState(() {
    widget.setNotes(notes);
    _markers = markers;
  });
}

Future<List<NCMBObject>> getNotes(LatLng location) async {
  // NCMBGeoPointに変換
  var geo = NCMBGeoPoint(location.latitude, location.longitude);
  // 検索用のクエリークラス
  var query = NCMBQuery('Note');
  // 位置情報を中心に3km範囲で検索
  query.withinKilometers('geo', geo, 3);
  // レスポンスを取得
  var ary = await query.fetchAll();
  // List<NCMBObject>に変換
  return ary.map((obj) => obj as NCMBObject).toList();
}
```

`_buildMarkerWidget` はNCMBObjectをウィジェットに変換する関数です。

```dart
// マーカーウィジェットを作成する
Future<Widget> _buildMarkerWidget(
    NCMBObject note, MapTransformer transformer) async {
  final geo = note.get("geo") as NCMBGeoPoint;
  final pos = transformer.toOffset(LatLng(geo.latitude!, geo.longitude!));
  final image = await NCMBFile.download(note.getString("image"));
  return Positioned(
    left: pos.dx - 16,
    top: pos.dy - 16,
    width: 40,
    height: 40,
    child: GestureDetector(
      child: SizedBox(
        child: Image.memory(image.data),
        height: 200,
      ),
      onTap: () => _onTap(pos.dy, pos.dx, note),
    ),
  );
}
```

表示したマーカーをタップした際には `_onTap` を呼びます。この関数では、画像付きのツールチップを表示します。

```dart
Future<void> _onTap(double top, double left, NCMBObject note) async {
  final image = await NCMBFile.download(note.getString('image'));
  final tooltip = Container(
    margin: const EdgeInsets.only(left: 15.0),
    padding: const EdgeInsets.symmetric(
      vertical: 5.0,
      horizontal: 10.0,
    ),
    child: Column(children: [
      Text(note.getString('text')),
      Text(note.getString('address', defaultValue: "不明") + "付近のメモ"),
      SizedBox(
        child: Image.memory(image.data),
        height: 200,
      )
    ]),
    decoration: const ShapeDecoration(
      color: Colors.white,
      shape: BubbleBorder(),
    ),
  );
  setState(() {
    _tooltip = Positioned(
      left: left - 170,
      top: top - 280,
      child: tooltip,
    );
  });
}
```

## メモの一覧表示

一覧画面 `ListPage` では `MainPage` から送られてきたメモデータを一覧表示します。メモを `RowPage` に送っているだけです。

```dart
class _ListPageState extends State<ListPage> {
  @override
  Widget build(BuildContext context) {
    return Center(
        child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Expanded(
          child: ListView.builder(
              itemBuilder: (BuildContext context, int index) =>
                  RowPage(note: widget.notes[index], location: widget.location),
              itemCount: widget.notes.length),
        )
      ],
    ));
  }
}
```

`RowPage` では受け取ったデータを表示します。画像データをダウンロードし、それも合わせて一覧に利用しています。また、Geolocatorを使って2点間の距離をメートルで算出しています。

```dart
// 設定画面
class _RowPageState extends State<RowPage> {
  Uint8List? _image;

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

  Future<void> _getImage() async {
    if (widget.note.get('image') == null) return;
    final fileName = widget.note.getString('image');
    final image = await NCMBFile.download(fileName);
    setState(() {
      _image = image.data;
    });
  }

  String distance() {
    final geo = widget.note.get('geo') as NCMBGeoPoint;
    final dist = Geolocator.distanceBetween(widget.location.latitude,
        widget.location.longitude, geo.latitude!, geo.longitude!);
    return dist.toStringAsFixed(0);
  }

  @override
  Widget build(BuildContext context) {
    return Row(children: [
      _image != null
          ? SizedBox(
              child: Image.memory(_image!),
              width: 150,
            )
          : const SizedBox(
              child: Icon(
              Icons.photo,
              size: 100,
              color: Colors.grey,
            )),
      const Padding(padding: EdgeInsets.only(left: 8)),
      Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        Text(widget.note.getString('text')),
        Text(widget.note.getString('address', defaultValue: '不明')),
        Text('${distance()}m')
      ])
    ]);
  }
}
```

## 今回利用したNCMBの機能

この地図メモアプリでは、NCMBの以下の機能を利用しました。

- データストア
    - データ登録
    - データ取得
- ファイルストア
    - アップロード
    - ダウンロード

NCMBには他にも認証、スクリプト、プッシュ通知などの機能があります。ぜひそれらの機能も利用してください。

## まとめ

今回はOpenStreetMapとFlutterを組み合わせて、位置情報を利用したメモアプリを作成しました。位置情報検索はNCMBの売り機能でもあるので、ぜひ利用してください。

[ドキュメント : 開発者向けドキュメント | ニフクラ mobile backend](https://mbaas.nifcloud.com/doc/current/#/JavaScript)