🐈

Google Places SDK for Androidを使って検索機能を実装する方法

2022/12/15に公開2

この記事はLuup Advent Calendarの15日目の記事です。

こんにちは、LuupのAndroidチームに業務委託としてジョインしている向中野(@mukky620)です。

今回は検索機能でAutocomplete検索を実装した際に使用したGoogle Places SDK for Androidを使った実装方法について簡単にご紹介したいと思います。

Google Places SDK for Androidとは

Google Places APIを使用するためのAndroid用SDKです。
主に下記のようなGoogle Map上のデータを取得する事が出来ます。

  • Autocomplete検索による予測場所リスト
  • 場所の名前や住所等の詳細情報
  • 現在地情報
  • 場所の写真情報

今回は上記のうち、LUUPの検索機能で使用しているAutocomplete検索に絞って紹介したいと思います。

セットアップ

APIの有効化やAPIキーの作成

Google Places APIを使用するにはGCPにてPlaces APIを有効にしたり、APIキーを作成したりと必要な作業がいくつかあります。
本記事ではそちらの詳細は割愛しますので、詳しくは公式のドキュメントを参照してください。

インストール

下記の依存を追加します。
※バージョンは執筆時点の最新です。

dependencies {
    implementation "com.google.android.libraries.places:places:2.7.0"
}

KTXも用意されているので使用する場合はこちらも追加します。

dependencies {
    implementation "com.google.maps.android:places-ktx:2.0.0"
}

APIキーをプロジェクトに追加

アプリからGoogle Places APIを使用する際にAPIキーを参照する必要があるのですが、安全に参照する方法としてSecrets Gradle Plugin for Androidを使用して参照する方法があります。
APIキーの追加に関しても詳しくは公式のドキュメントを参照してください。

Places API clientの初期化

ApplicationクラスでPlaces SDK for Androidを初期化します。
第2引数には先述したAPIキーをセットします。

    // Initialize the SDK
    Places.initialize(applicationContext, apiKey)

また、実際に使用するクラスでPlacesClientを生成しておきます。
DIを使用している場合や頻繁にPlacesClient生成をする場合は、Singletonで保持しても良いかもしれません。

    // Create a new PlacesClient instance
    val placesClient = Places.createClient(this)

実装

Autocomplete検索

入力した文字列からAutocomplete検索をする検索機能を実装する場合、下記の3種類の方法があります。

  1. AutocompleteSupportFragment をレイアウトに埋め込んで使用する。
    • SDKで用意されたAutocompleteWidgetをそのまま使用できるので、開発コストは少ない
    • 検索Widgetの自由なカスタムが難しい
  2. Autocomplete.IntentBuilder を使用して専用のActivityを起動する。
    • SDKで用意されたAutocompleteWidgetをそのまま使用できるので、開発コストは少ない
    • 検索Widgetの自由なカスタムが難しい
  3. PlacesClient.findAutocompletePredictions() を使用してデータを取得する。
    • 検索Widgetの自由なカスタムが可能
    • プログラムでデータを取得するので検索Widgetやロジック等を独自で実装する必要があり、開発コストは少し高い

3種類の方法には先述した通りそれぞれメリットとデメリットがありますので、サービス仕様に合わせて選定すると良いと思います。
LUUPの検索機能では、デザイン仕様を満たすために自由なカスタムができる3.の方法を採用しましたのでそちらの方法について紹介します。

PlacesClient.findAutocompletePredictions()での場所情報取得

PlacesClient.findAutocompletePredictions()FindAutocompletePredictionsRequestを指定する事でデータを取得するAPIです。

FindAutocompletePredictionsRequest

パラメータ 必須 説明
query String 入力した文字列
sessionToken AutocompleteSessionToken セッション化するために使用されるトークン
詳しくはこちらを参照
cancellationToken CancellationToken 操作をキャンセルする必要がある場合に使用するトークン
locationBias LocationBias 緯度と経度の境界を指定して領域内の結果を優先する
locationRestriction LocationRestriction 緯度と経度の境界を指定して領域内の結果のみを表示する
countries List<String> 結果の場所を制限する国のリスト
最大で5か国までサポート
typesFilter List<String> 結果のタイプを制限するフィルター
サポートされているタイプはPlaceTypesを参照
origin LatLng リクエストの起点となる位置情報を指定
指定する事でdistanceMetersという指定された起点との間の直線距離をメートル単位で取得できる

FindAutocompletePredictionsRequestを指定して下記のようにAPIをコールし、データを取得します。
※位置情報の値はサンプルとして適当なものを代入しています。

    // 文字列入力して決定するまでを1セッションとしてトークンとする事で無駄なAPIリクエスト費用を減らす
    val token = AutocompleteSessionToken.newInstance()

    // LocationBiasに設定する位置情報を生成
    val bounds = RectangularBounds.newInstance(
        LatLng(-33.880490, 151.184363),
        LatLng(-33.858754, 151.229596)
    )
    val request =
        FindAutocompletePredictionsRequest.builder()
            // LocationBias() または LocationRestriction()のどちらかを指定する事を推奨
            .setLocationBias(bounds)
            .setOrigin(LatLng(-33.8749937, 151.2041382))
            .setCountries("jp")
            .setTypesFilter(listOf(PlaceTypes.ESTABLISHMENT))
            .setSessionToken(token)
            .setQuery(query)
            .build()
    placesClient.findAutocompletePredictions(request)
        .addOnSuccessListener { response: FindAutocompletePredictionsResponse ->
            for (prediction in response.autocompletePredictions) {
                Log.i(TAG, prediction.placeId)
                Log.i(TAG, prediction.getPrimaryText(null).toString())
            }
        }.addOnFailureListener { exception: Exception? ->
            if (exception is ApiException) {
                Log.e(TAG, "Place not found: " + exception.statusCode)
            }
        }

また、KTXのPlacesClient.awaitFindAutocompletePredictions()を使用して下記のように取得もできます。

    val token = AutocompleteSessionToken.newInstance()

    val bounds = RectangularBounds.newInstance(
        LatLng(-33.880490, 151.184363),
        LatLng(-33.858754, 151.229596)
    )
    runCatching {
        placesClient.awaitFindAutocompletePredictions {
            locationBias = bounds
            origin = LatLng(-33.8749937, 151.2041382)
            countries = listOf("jp")
            typesFilter = listOf(PlaceTypes.ESTABLISHMENT)
            sessionToken = token
            query = keyword
        }
    }
        .onSuccess {
            for (prediction in it.autocompletePredictions) {
                Log.i(TAG, prediction.placeId)
                Log.i(TAG, prediction.getPrimaryText(null).toString())
            }
        }
        .onFailure {
            if (it is ApiException) {
                Log.e(TAG, "Place not found: " + exception.statusCode)
            }
        }
    }

LUUPでは下記の5つのパラメーターを指定してリクエストしています。

  • API費用の最適化のためのsessionToken
  • 指定した文字列の予測場所をより近い場所から優先するためのlocationBias
  • 予測場所を日本国内に絞るためのcountries
  • 予測場所を分かりやすい場所名や建物名で表示するためのtypesFilter(ESTABLISHMENTを指定)
  • 予測場所を現在地から近い順に表示するためのorigin

そして、上記のAPIのResponseのFindAutocompletePredictionsResponseに、文字列から予測された場所情報がリストで返却されます。

FindAutocompletePredictionsResponse

パラメータ 説明
autocompletePredictions List<AutocompletePrediction> 場所情報リスト
AutocompletePrediction
メソッド 説明
getFullText(CharacterStyle) SpannableString 場所の説明の全文
例:「エッフェル塔、アベニュー アナトール フランス、パリ、フランス」
getPrimaryText(CharacterStyle) SpannableString 場所を説明する本文
例:「エッフェル塔、123 ピット ストリート」
getSecondaryText(CharacterStyle) SpannableString 場所の説明の補助テキスト
例:「アベニュー アナトール フランス、パリ、フランス、シドニー、ニュー サウス ウェールズ」
getPlaceId() String 予測された場所の場所ID
場所を一意に識別するテキスト識別子でこのplaceIDを使用してさらに詳細情報を取得する事ができる
詳しくはこちら
getPlaceTypes() List<Type> 関連付けられたプレイスタイプのリスト
getDistanceMeters() Int この場所とリクエストで指定された起点との間の直線距離(メートル単位)

上記のFindAutocompletePredictionsResponseの予測された場所情報リストをparseしてデータを参照する事で、検索結果として表示します。

LUUPでは、getPlaceId()getPrimaryText(CharacterStyle)getSecondaryText(CharacterStyle)getDistanceMeters()などを使用してソートしたりResponseデータを内部データにparseしたりする事で最終的に下のスクショのように検索結果として表示しています。
Luup-search-sample-picture

終わりに

以上がGoogle Places SDK for Androidのセットアップから場所情報のデータを取得して表示するまでの簡単な流れとなります。
最初のセットアップ部分さえすんなり完了できれば意外と簡単に実装できると思います。

また、今回は省略しましたが
・検索結果をタップしたタイミングで、対象の場所の詳細情報を取得してマップを移動させる
・検索文字を1文字入力するたびにPlaces APIをコールしないように上手く間引く
などの処理も行なっていたりするので機会があればご紹介できればと思います。

今後、検索機能をユーザーの皆様がさらに使いやすい機能になるようにブラッシュアップを重ねていこうと考えていますので期待して待っていただければなと思います。

Luup Developers Blog

Discussion

nagaoyurikonagaoyuriko

はじめましてm(__)m

PlaceApiど初心者です

やりたいことが
「latとlngから、placeIdを特定(取得)する」
なのですが

方法あると思いますかm(__)m?

AutocompletePredictionにgetPlaceId()というものはあるが、AutocompletePredictionのビルダーにplaceId食わせないとインスタンス作れない、でグルグルしておりますm(__)m

(コメントするのにアカウント必要で、zennデビューさせて頂きましたm(__)m)

TaroTaro

質問です。
私は初期画面に地図をFragmentを利用して表示させて
同じ初期画面に入力のための検索窓?Widget?部分を作成し
検索窓部分押下で検索窓が立ち上がるように作成しました。

初期地図画面に検索窓につながるボタンをのっけて地図を表示させたい場合
どういう処理で実現可能でしょうか。
Fragmentでは無理ですかね。。。