概要

今回PlacesAPIのNearBySearchを使って初めて開発しました。
公式ドキュメントの内容を見れば概要は理解できますが、Flutterでどう使うのかについて説明している記事は少ないように感じます。
本記事はNearBySearchについて整理し、Flutterでリクエスト方法含めてどう扱えば良いか理解することを目的とします。

この記事で伝えたいこと

  • NearBySearchで実現できること。
  • 最大何件まで取得できるのか。
  • 店舗の営業時間を取得する方法。

解決したい課題と経緯

今回のプロジェクトでは、「現在地から特定の施設(コンビニや公園など)を近い順に表示させる」という要件があり、フレームワークはFlutterでPlacesAPIを利用するという制限事項がありました。
FlutterでPlacesAPIのNearBySearchを使う場合、調査時に要件を満たすpluginが無かったことに加えhttpリクエストを実行するのみのため、Maps Javascript APIを直接コールする形で実装したという経緯があります。

環境

  • OS: iOS
  • フレームワーク: Flutter (3.13.0)
  • Xcode: 15.0.1

前提

  • APIKey取得済みであること。
  • PlacesAPI有効化済みであること。

NearBySearchとは

Nearby Search では、指定された範囲内で場所を検索できます。
キーワードを指定するか、検索する場所のタイプを指定することで、検索リクエストを絞り込むことができます。

公式リンク

以下keywordおよびlocationに任意の値をセットすることで「現在地から公園」「自宅からカフェ」といった情報を近い順に取得できます。

主なパラメータ

key 作成したAPIKeyをセット。
秘匿情報のため、環境変数に設定して値を取得してくることをお勧めします。
language 結果を返す言語コード。多言語対応なしであればjaでOK。
keyword 場所の名前や住所、施設のカテゴリー名など。
location 指定場所の緯度/経度。
rankby 結果を表示させる順序を指定できる。
  • rankby=prominence
    重要度に基づいて結果を並べ替えて取得できる。
    知名度や口コミ、人気によって結果は異なる。半径パラメータ(radius)が必要。
  • rankby=distance
    指定した場所からの距離に応じて検索結果を昇順に取得できる。

注意点

rankby = distanceを指定する場合、radiusは指定不可になります。

RankBy.DISTANCE を指定した場合、カスタムの bounds と radius は指定できません。

公式リンク

実装1

const keyword = 'コンビニ';
// 東京タワーの緯度経度
const latitude = 35.6585805;
const longitude = 139.742858;
// 東京タワーから近い順にコンビニの情報を取得
final response = await http.get(
 Uri.parse(
  'https://maps.googleapis.com/maps/api/place/nearbysearch/json?key=$apiKey&language=ja&keyword=$keyword&location=$latitude,$longitude&rankby=distance',
 ),
);
final bodyMap = jsonDecode(response.body) as Map<String, dynamic>;

最大何件まで取得可能?

上記実装1では最大20件の情報を取得できますが、GoogleMap上にコンビニのピンを複数表示させたいとなった場合、20件では足りないという課題がありました。
それを解決するためには、以下公式記載の制約事項に従って追加でリクエストする必要があります。

デフォルトでは、各 Place Search は 1 つのクエリに対して最大 20 件の結果を返します。ただし、各検索では、3 ページに分けて最大 60 件の結果を返すことができます。

2ページ,3ページ目に関するリクエストを実行するには、以下公式記載の通りnextPageTokenが必要になります。

最大 20 件の追加結果を返すために使用できるトークンが含まれています。 表示する追加の結果がない場合、next_page_token は返されません。 返される結果の最大数は 60 です。 next_page_token が発行されてから有効になるまでには、少し時間がかかります。

 

 

追加結果取得の流れ

  1.   1度目のAPIリクエストを実行
  2.   21件以上を取得->nextPageTokenあり、20件未満の場合->nextPageTokenなし
  3.   nextPageTokenがあれば、2度目のAPIリクエストを実行
  4.   取得した結果を配列にaddしてまとめる

実装(実装1が完了した前提)

final hasNextPage21To40 = bodyMap.containsKey('next_page_token');
if (hasNextPage21To40) {
 final nextToken21To40 = bodyMap['next_page_token']; // bodyMapは実装1参照
 final response21To40 = await http.get(
  Uri.parse
   'https://maps.googleapis.com/maps/api/place/nearbysearch/json?key=$apiKey&pagetoken=$nextToken21To40', // language, keyword, location, rankbyは省略可。1度目のリクエストで指定済みのため。
 ),
 Map<String, dynamic> bodyMap21To40;
 bodyMap21To40 = jsonDecode(response21To40.body) as Map<String, dynamic>;
 final valueBelow20 = bodyMap['results'];
 valueBelow20.addAll(bodyMap21To40['results']);
}

店舗の営業時間取得

GoogleMap上にコンビニのピンを複数表示させ、ピンをタップした際に店舗の営業時間情報を表示させるという要件がありました。
実際に実装1のレスポンスを確認すると営業時間情報は含まれておらず、別の方法で取得する必要があるという課題に直面しました。
以下公式記載の通り、NearBySearchリクエストとは別でplaceIdをもとにPlaceDetailsを実行することで課題は解決できます。

Find Place、Nearby Search、Text Search のリクエストではすべて、Place Details リクエストで返されるフィールドのサブセットが返されます。これらのメソッドでは、次のフィールドは返されません。

  • opening_hours.weekday_text

OpeningHoursweekday_textには曜日に対する営業時間の情報が含まれています。

 

実装(実装1でplaceidが取得できている前提)

final response = await http.get(
 Uri.parse(
  'https://maps.googleapis.com/maps/api/place/details/json?placeid=$placeId&key=$apiKey',
 ),
);

その他

  • 多言語対応時、PlacesAPIで定義されてない言語(he: ヘブライ語など)を端末で設定した場合、表示は英語になりました。(個人確認) PlacesAPI定義言語一覧
  • API仕様は頻繁に変更が入るためご参考にされる際は一度公式ドキュメントに目を通されることをお勧めします。

まとめ

今回PlacesAPIを使ったのは初めてでしたが、様々なシステムに導入できそうな面白い仕組みでかつドキュメントの内容が充実しており、開発していて楽しかったです。
GoogleMapのAPIは他にもたくさんあるので興味がある方は挑戦してみると良いでしょう。