🥚

Androidで一回位置情報を取得するボタンを作る

2023/08/18に公開

はじめに

Androidで位置情報を取得する手段はいくつかあります。
そのうちAndroid公式が推奨するFused Locationを使った方法を紹介します。

Fused Locationの前提知識

端末が直前に取得した位置情報を返す

Fused Locationは”端末が直前に取得した位置情報”を取得します。
つまり位置情報取得リクエストした時に値が変わるとは限りません。

例えば、端末が5秒間に一度位置情報を更新するとします。
そしてアプリ内で1秒間に一度位置情報をリクエストするとします。
この時、アプリ内では5回連続で同じ位置情報の値が返されます。

位置情報は複数の情報から導き出される

位置情報と言えばGPSですが、Fused LocationはGPS以外にも多くのセンサデータに基づいて位置情報を推定します。
結論から言うと、ネットワークに接続しているかどうかで位置情報の推定精度が変わります。
詳しくはなぜGPSが届かないはずの駅構内でも地図アプリは完璧に動くのか
もしくは位置情報を最適化して電池の消耗を抑える

1:プロジェクトとクラスを作る

まずはAndroid Studioを立ち上げてプロジェクトを作りましょう。
できる人は飛ばしてください。

プロジェクト作成 ~ クラス作成

Android Studioを立ち上げて ”New Project”

Empty View Activityを選択。

アプリの名前やパッケージ名を入力。

プロジェクトが作成されたら
File → New → Kotlin Class/File

クラスの名前は 'Location Sensor'にしておきましょう

プログラム

このアプリはプログラムの他にライブラリや権限のリクエストが必要です。

まずはplay-services-locationの依存関係を追加しましょう。
一番下の
implementation 'com.google.android.gms:play-services-location:20.0.0'
を追加しましょう

そして追加後に右上に出る Sync Nowを押しましょう。

build.gradle(Module app)
dependencies {

    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.5.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    
    // Fused Location
    implementation 'com.google.android.gms:play-services-location:20.0.0'
}

次に権限のリクエストをします。
位置情報はセンシティブな情報なので、リクエストするときにユーザから許可してもらう必要があります。

AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!--この下の二つを追加!-->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.GPSApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

それでは位置情報を取得するクラスを作ります。
クラスを分けずMainActivityに書いても大丈夫ですが、いずれちゃんとクラス分けしないと大変なことになるので分けていきましょう。

LocationSensor
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.location.Location
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices

class LocationSensor(val activity: MainActivity) {

    // 位置情報が更新されたらこのLiveDataに格納する
    private val _location: MutableLiveData<Location> = MutableLiveData<Location>()
    val location: LiveData<Location> = _location

    fun requestLocationPermission(activity: Activity) {
        val LOCATION_PERMISSION_REQUEST_CODE = 1001

        // 位置情報の権限があるか確認する
        val isAccept = ContextCompat.checkSelfPermission(
            activity,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED

        if (!isAccept) {
            // 権限が許可されていない場合はリクエストする
            ActivityCompat.requestPermissions(
                activity,
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
                LOCATION_PERMISSION_REQUEST_CODE
            )
        }
    }

    fun fusedLocation() {

        // 最後に確認された位置情報を取得
        val fusedLocationClient: FusedLocationProviderClient =
            LocationServices.getFusedLocationProviderClient(activity)

        // 一応権限のチェック
        if (ActivityCompat.checkSelfPermission(
                activity,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
            && ActivityCompat.checkSelfPermission(
                activity,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            Log.d("LocationSensor","権限がない")
            // 権限もらえないと困っちゃうなぁ
            return
        }

        // 位置情報を取得したらListenerが反応する
        fusedLocationClient.lastLocation
            .addOnSuccessListener(activity) { location ->
                Log.d("LocationSensor","$location")
                if (location != null) {
                    _location.postValue(location)
                }
            }
    }
}

そしたらMainActivityとLayoutを作っていきましょう。
まずはLayoutです。

位置情報をリクエストするボタンと位置情報を表示するテキストを用意しましょう。

activity_main

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textview" />

</androidx.constraintlayout.widget.ConstraintLayout>

最後にMainActivityでLocationSensorを呼び、ボタンやテキストと連携します。

MainActivity
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.lifecycle.Observer

class MainActivity : AppCompatActivity() {

    lateinit var textView: TextView
    lateinit var button: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textView = findViewById(R.id.textview)
        button = findViewById(R.id.button)

        val locationSensor = LocationSensor(this)
        locationSensor.requestLocationPermission(this)
        locationSensor.fusedLocation()

        // LiveDataの値が変化をobserverが監視、プログラムを実行する
        locationSensor.location.observe(this, Observer {
            textView.text = "${it.latitude}\n, ${it.longitude}"
        })

        button.setOnClickListener {
            locationSensor.fusedLocation()
        }

    }
}

おわりに

今回は位置情報をボタンを押した時、一度だけ取得するアプリを作りました。
位置情報を一度だけ取得するユースケースはあまり少ないですね。
例えば近くのお店を検索する、とかでしょうか。

Fused Locationにはちゃんと一定間隔で位置情報をリクエストする関数が用意されています。
なので次は位置情報を継続的に取り続けるアプリを作ってみましょう。

Discussion