NCMBのFlutter SDKを使って地図検索アプリを作る（その2：位置情報をインポートする）

NCMBでは公式SDKとしてSwift/Objective-C/Java/Unity/JavaScript SDKを用意しています。また、それ以外にもコミュニティSDKとして、非公式ながらFlutter/React Native/Google Apps Script/C#/Ruby/Python/PHPなど幅広い言語向けにSDKが開発されています。

今回はコミュニティSDKの一つ、Flutter SDKを使って地図検索アプリを作ってみます。[前回は画面の仕様とSDKの初期化について解説しました](https://qiita.com/goofmint/items/339c8ea18620e955916d)。今回は位置情報をデータストアにインポートする流れを解説します。

## 完成版のコード

作成したデモアプリのコードは[NCMBMania/flutter_map_app](https://github.com/NCMBMania/flutter_map_app)にアップロードしてあります。

## インポート画面について

インポート画面はメイン画面のタブで読み込まれています。 `SettingPage` がそうです。

```dart
class _MainPageState extends State<MainPage> {
  // タイトル
  final title = '地図アプリ';

  // 表示するタブ
  final _tab = <Tab>[
    const Tab(text: '地図', icon: Icon(Icons.map_outlined)),
    const Tab(text: 'インポート', icon: Icon(Icons.settings)),
  ];

  // AppBarとタブを表示
  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tab.length,
      child: Scaffold(
        appBar: AppBar(
          title: Text(title),
          bottom: TabBar(
            tabs: _tab,
          ),
        ),
        body: const TabBarView(children: [
          MapPage(),
          SettingPage(),
        ]),
      ),
    );
  }
}
```

SettingPageは_SettingPageStateを読み込んでいるだけです。

```dart
// 設定画面用のStatefulWidget
class SettingPage extends StatefulWidget {
  const SettingPage({Key? key}) : super(key: key);
  @override
  State<SettingPage> createState() => _SettingPageState();
}
```

## インポート画面の構築

まず `_SettingPageState` のクラス変数についてです。文字列の配列が入る _logs と データをインポート（または削除）するNCMBのデータストアクラス名を入れた _className があります。

```dart
// 設定画面
class _SettingPageState extends State<SettingPage> {
  final List<String> _logs = [];
  final String _className = 'Station';
```

UIは次のようになります。インポートボタンをタップすると `importGeoPoint` が実行されます。また、_logs が更新されると ListView.builder を使ってログが表示されます。

```dart
@override
Widget build(BuildContext context) {
	return Center(
			child: Column(
		mainAxisAlignment: MainAxisAlignment.center,
		children: [
			const Text(
				'山手線のデータをインポートします',
			),
			TextButton(onPressed: importGeoPoint, child: const Text('インポート')),
			Expanded(
				child: ListView.builder(
						itemBuilder: (BuildContext context, int index) =>
								Text(_logs[index]),
						itemCount: _logs.length),
			)
		],
	));
}
```

![localhost_53739_ (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/621335c2-9263-a14c-cad5-ed2b45dbb301.png)


## データのインポートについて

データをインポートする際には繰り返し処理できるように、まず既存データを削除しています。もし既存データがなかった場合でも（データストアのクラスがなくとも）エラーにはならず、空の配列が返ってくるだけです。

```dart
// 位置情報のJSONを取り込む処理
Future<void> importGeoPoint() async {
	// ログを消す
	setState(() {
		_logs.clear();
	});
	// 全駅情報を消す
	deleteAllStations();
	// 省略
}
```

`deleteAllStations` は以下のようになります。NCMBQueryを使ってデータを取得し、各データ（NCMBObject）のdeleteメソッドを使ってデータを削除します。

```dart
// データストアからすべての駅情報を削除
Future<void> deleteAllStations() async {
	// 駅情報を検索するクエリークラス
	final query = NCMBQuery(_className);
	// 検索結果の取得件数
	query.limit(100);
	// 検索
	final ary = await query.fetchAll();
	// 順番に削除
	for (var station in ary) {
		station.delete();
	}
}
```

データを削除したら、山手線の各駅の位置情報が入ったJSONファイルを読み込みます。

```dart
// importGeoPointの内容です。
// JSONファイルを読み込む
String loadData = await rootBundle.loadString('json/yamanote.json');
final stations = json.decode(loadData);
// JSONファイルに従って処理
stations.forEach((params) async {
	// 駅情報を作成
	var station = await saveStation(params);
	// ログを更新
	setState(() {
		_logs.add("${station.get('name')}を保存しました");
	});
});
```

`saveStation` は以下のようになります。JSONファイルのパラメータを渡して、Stationクラスにデータを作成します。NCMBGeoPointを使って位置情報を保存するのがポイントです。

JSONファイルの内容は次のようなデータです。

```json
[
  {
    "name": "東京",
    "latitude": "35.681382",
    "longitude": "139.76608399999998"
  },
  {
    "name": "有楽町",
    "latitude": "35.675069",
    "longitude": "139.763328"
  },
	// 省略
]
```

saveStationの内容です。

```dart
// 駅情報を作成する処理
Future<NCMBObject> saveStation(params) async {
	// NCMBGeoPointを作成
	var geo = NCMBGeoPoint(
			double.parse(params['latitude']), double.parse(params['longitude']));
	// NCMBObjectを作成
	var station = NCMBObject(_className);
	// 位置情報、駅名をセット
	station
		..set('name', params['name'])
		..set('geo', geo);
	// 保存
	await station.save();
	return station;
}
```

![localhost_53739_ (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/e9d544ac-e66e-cf0f-f848-0b3e426d0b80.png)


## 全体のコード

SettingPage、_SettingPageStateクラスの内容は次のようになります。

```dart
// 設定画面用のStatefulWidget
class SettingPage extends StatefulWidget {
  const SettingPage({Key? key}) : super(key: key);
  @override
  State<SettingPage> createState() => _SettingPageState();
}

// 設定画面
class _SettingPageState extends State<SettingPage> {
  final List<String> _logs = [];
  final String _className = 'Station';

  // データストアからすべての駅情報を削除
  Future<void> deleteAllStations() async {
    // 駅情報を検索するクエリークラス
    final query = NCMBQuery(_className);
    // 検索結果の取得件数
    query.limit(100);
    // 検索
    final ary = await query.fetchAll();
    // 順番に削除
    for (var station in ary) {
      station.delete();
    }
  }

  // 位置情報のJSONを取り込む処理
  Future<void> importGeoPoint() async {
    // ログを消す
    setState(() {
      _logs.clear();
    });
    // 全駅情報を消す
    deleteAllStations();
    // JSONファイルを読み込む
    String loadData = await rootBundle.loadString('json/yamanote.json');
    final stations = json.decode(loadData);
    // JSONファイルに従って処理
    stations.forEach((params) async {
      // 駅情報を作成
      var station = await saveStation(params);
      // ログを更新
      setState(() {
        _logs.add("${station.get('name')}を保存しました");
      });
    });
  }

  // 駅情報を作成する処理
  Future<NCMBObject> saveStation(params) async {
    // NCMBGeoPointを作成
    var geo = NCMBGeoPoint(
        double.parse(params['latitude']), double.parse(params['longitude']));
    // NCMBObjectを作成
    var station = NCMBObject(_className);
    // 位置情報、駅名をセット
    station
      ..set('name', params['name'])
      ..set('geo', geo);
    // 保存
    await station.save();
    return station;
  }

  @override
  Widget build(BuildContext context) {
    return Center(
        child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        const Text(
          '山手線のデータをインポートします',
        ),
        TextButton(onPressed: importGeoPoint, child: const Text('インポート')),
        Expanded(
          child: ListView.builder(
              itemBuilder: (BuildContext context, int index) =>
                  Text(_logs[index]),
              itemCount: _logs.length),
        )
      ],
    ));
  }
}
```

## まとめ

今回はJSONファイルをインポートし、それを使ってNCMBObjectを作成しました。[次回は地図コンポーネントと合わせて、位置情報検索を実装](https://qiita.com/goofmint/items/a1f70c4167188bf03bfd)します。