Fused Location ProviderでKotlin FlowとLiveDataを再入門してみた

# 前書き

CodeLabの[Building a Kotlin extensions library](https://codelabs.developers.google.com/codelabs/building-kotlin-extensions-library/index.html)をやってみました。

名前からしてライブラリの作り方のベストプラクティスじゃないかと思って、勉強しようと思ったら拡張関数の内容でした。
でも拡張関数よりも、中で使ってるFlowのほうがすごかったです。

勉強したいものと違いましたが、改めてFlowの凄さを再認識できたので記事にして記録を残そうと思いました。

# このCodeLabの内容は？

このCodeLabはリアルタイムで地理情報を表示するアプリを作っています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/57546/c7b4e132-0f95-9c2a-47d3-0f9761669b2b.gif" width="300">

数秒ごとにLocationを更新しています。
やろうとしていることは結構かんたんですが、**結構クセがあるAPI**と、**Androidのライフサイクル**の特有の問題があるので、気をつけて書かないと結構かんたんに罠にハマります。（ライフサイクルの難しさはこの記事であまり解説しないつもり）

一応このCodeLabのタイトルは「Building a Kotlin extensions library」なので、拡張関数のすごさを説明するものでしたが、中で使ってるFlowがすごすぎて、拡張関数なんて目じゃないレベルでした。

# 1. Flowを使わないJavaのAPI

地理情報を表示するのにLocationServicesを使います。このAPIはJavaで書かれているので、Callbackを前提とした使い方になっています。
使い方として

- リアルタイムのLocation情報を取得する[^requestupdates]
- 前回取得した最後のLocationを取得する[^lastlocation]

があります。

「リアルタイムのLocation情報を取得する」でも十分ですが、最初の更新まで時間がかかるのと、回線不安定などでデータを上手く取得できない場合もあるので、だいたい「前回取得した最後のLocationを取得する」と組み合わせて使うイメージです。

## 前回取得した最後のLocationを取得する

```kotlin
val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) // Activity or Context

fusedLocationProviderClient.lastLocation.addOnSuccessListener {
       // 成功時の処理
       ...
   }.addOnFailureListener {
       // 失敗時の処理
       // 位置情報の権限がない場合はここに来る
       ...
   }
```

なぜか`lastLocation`の戻り値は`Task<Location?>`なので、結果を`addOnSuccessListener`と`addOnFailureListener`で非同期で取得しなければいけない。

## リアルタイムのLocation情報を取得する

```kotlin
val callback = object: LocationCallback() {
    override fun onLocationResult(result: LocationResult?) {
        // コールバックで画面等を更新する
    }
}

// Locationを取得する頻度を設定する
val locationRequest = LocationRequest().apply {
    interval = 3000
    fastestInterval = 2000
    priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}

val fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) // Activity or Context

// 上記の設定をセットアップして、更新を開始する
fusedLocationProviderClient.requestLocationUpdates(
    locationRequest,
    callback,
    Looper.getMainLooper()
).addOnFailureListener {
    // 更新が失敗したときにここに来る
}

// 使用完了後（例えばActivity#onDestroy, Activity#onStop）は不要な更新を止める
fusedLocationProviderClient.removeLocationUpdates(callback)
```

Callback内で更新したLocationを使うので、RxもしくはFlowなどのリアクティブプログラミングの力を借りなければ、setTextとかのコードをCallbackの中に書いてしまいそうです。そうなるとメンテナンスしづらいコードになります。

また、`Activity#onDestroy`もしくは`Activity#onStop`でCallbackを**削除しなくてもコケはしない**ですが（一応Callback内でやってることによります）、常時Locationを使う必要がなければ**無駄に電池を使うことになる**ので、止めてあげたほうがいいです。

# 2. Coroutine / FlowでJavaの非同期APIをラップする

## 前回取得した最後のLocationを取得する

`lastLocation: Task<Location?>`（※１）の非同期は一回限りの処理なので、Flowじゃなくて、Coroutineで対応します。

ここでは`FusedLocationProviderClient`の拡張関数にして、`fusedLocationProviderClient.lastLocation`を呼ぶ代わりに`fusedLocationProviderClient.awaitLastLocation()`のsuspend functionで非同期の値を取得できます。

```kotlin
suspend fun FusedLocationProviderClient.awaitLastLocation(): Location? =
   suspendCancellableCoroutine<Location> { continuation ->
       lastLocation.addOnSuccessListener { location ->
           continuation.resume(location)
       }.addOnFailureListener { e ->
           continuation.resumeWithException(e)
       }
   }
```

使うときは例えばこんな感じで使います。

```kotlin
lifecycleScope.launch {
    try {
        val location = fusedLocationProviderClient.awaitLastLocation()
        ...
    } catch (e: Exception) {
        Log.d(TAG, "Unable to get location", e)
   }
}
```

※１: ここのCodeLabのコードが間違っているようです。CodeLabはNull非許容型で書いていましたが、Successの場合でもnullが帰ってくることがあるそうです[^lastlocation]

## リアルタイムのLocation情報を取得する

リアルタイムで定期的にデータが流れてくるので、ここはFlowの出番です。使用するBuilderは`callbackFlow<T>`です。

```kotlin
fun FusedLocationProviderClient.locationFlow() = callbackFlow<Location> {
    val callback = object: LocationCallback() {
        override fun onLocationResult(result: LocationResult?) {
            result ?: return
            for (location in result.locations) {
                offer(location) // emit location into the Flow using ProducerScope.offer
            }
        }
    }

    requestLocationUpdates(
        createLocationRequest(),
        callback,
        Looper.getMainLooper()
    ).addOnFailureListener { e ->
        close(e) // in case of error, close the Flow
    }

    awaitClose {
        removeLocationUpdates(callback) // clean up when Flow collection ends
    }
}
```

ここでのポイントは、
1. Flowが閉じられたら`awaitClose`で**データの更新を止める**
2. 例外が投げられたらFlowを止める
3. （そしてFlowの性質上、購読する人がいなくなれば自動的に閉じられるので、リソースリークにならない）

使う側は例えばActivity内で購読する場合はこうなります。

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    // よくないコード。後述
    lifecycleScope.launch {
        fusedLocationClient.locationFlow()
            .catch { e ->
                // 例外処理
            }
            .collect { location ->
                // 成功時のUIの更新など
            }
    }
}
```

これでUIを更新する処理とLocationを非同期で取得する処理に分けられたので、スッキリ書けました。
ただし、上記のコードには実は一点問題が残っています。`lifecycleScope.launch`は`Activity#onDestroy`が呼ばれるまで死なないので、**Activityの画面が見えなくても**（例えばバックグラウンドに移したなど）動き続けて、**無駄にflowから値を取得し続けます**。クラッシュはしないですが、**無駄に電力を消耗しています**。

~~`launch`の代わりに`[Lifecycle.State.STARTED]`[^lifecycle]の時しか動かない`launchWhenStarted`を使えばこの問題を回避できます[^coroutine]。~~ 間違い。確かに`launchWhenStarted`は`[Lifecycle.State.STARTED]`時にしか発動しないですが、Activityが死ななかったら`lifecycleScope`自体が死ぬわけじゃないので、**Flowはキャンセルされません**し、`awaitClose`も**呼ばれません**。

### LiveDataを購読する

問題を回避するために`Flow<T>.asLiveData()`を使って**FlowをLiveDataに変換して**使えば、
Flowの生存期間・ライフサイクルの管理をLiveDataが自動でやってくれます。

- `[Lifecycle.State.STARTED]`の状態じゃなくなれば**upstream flowを閉じて**[^aslivedata]くれる
- `[Lifecycle.State.STARTED]`の状態に戻れば**もう一度upstream flowを実行して**くれる

このため、View側で使うFlowでしたら、全部`Flow<T>.asLiveData()`でLiveDataに変換してから使ったほうが良いでしょう。

```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
    fusedLocationClient.locationFlow()
        .conflate() // 購読する側の処理が遅かったら値を破棄する。意味がわからなければ読み飛ばして大丈夫
        .catch { e ->
            // 例外処理
        }
        .asLiveData() // ★ Flow<T>をLiveData<T>に変換する
        .observe(this) { location ->
            // 成功時のUIの更新など
        }
}
```

# 完成したコード

CodeLabのサンプルなので、すでにGitHubにあります。
この[CodeLab](https://codelabs.developers.google.com/codelabs/building-kotlin-extensions-library/index.html)すごく良いので、ぜひやってみてください。

https://github.com/googlecodelabs/kotlin-coroutines/blob/b71b981f8354a92fb3ebda37eceb7461e783d0bf/ktx-library-codelab/step-06/app/src/main/java/com/example/android/kotlinktxworkshop/MainActivity.kt

一応このサンプルではViewModelを使ってないし、`awaitLastLocation()`と`locationFlow()`を別々で取得してくるので、シンプルに書けてない問題が残っています。 ~~もう少し改造してきれいに書けるサンプルを別途上げる予定です。~~ [続きの記事](https://qiita.com/ibara1454/items/8e426ba8fdd95f1993c1)を書きました。よかったら見てください。

[^requestupdates]: Developers - Request updates https://developer.android.com/training/location/request-updates
[^lastlocation]: Developers - Get the last known location https://developer.android.com/training/location/retrieve-current
[^lifecycle]: Developers - Handling Lifecycles with Lifecycle-Aware Components https://developer.android.com/topic/libraries/architecture/lifecycle
[^coroutine]: Developers - Use Kotlin coroutines with Architecture components https://developer.android.com/topic/libraries/architecture/coroutines
[^aslivedata]: FlowLiveData.kt - https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/FlowLiveData.kt#110