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

本記事は[Fused Location ProviderでKotlin FlowとLiveDataを再入門してみた](https://qiita.com/ibara1454/items/cd2322fdfcfc709538e9)の続きです。

このCodeLabのサンプル[Building a Kotlin extensions library](https://codelabs.developers.google.com/codelabs/building-kotlin-extensions-library/index.html)はおそらく初心者でも読めるようにするため、あえてViewModelとかを使っていませんが、そんなのこっちの知ったこっちゃない、本気を出していい感じに改造しました。ただの自己満足ですのであしからず。

# 何をやったの？

やってることはすごく簡単です。

- Locationの値は`awaitLastLocation()`と`locationFlow()`で別々だったが、**まとめて一本のflowにした**
- Flowを構築するコードをUIから排除するため、**ViewModelを導入した**
- UI側にLiveDataを`observe`するコードがあったが、それを省略するため**DataBindingを導入した**

## 1.改造前のコード

（一部`ACCESS_FINE_LOCATION`権限を取得するためのコードとログを省略しています）
以下[Building a Kotlin extensions library](https://codelabs.developers.google.com/codelabs/building-kotlin-extensions-library/index.html)を完了させた状態のコードです。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var fusedLocationClient: FusedLocationProviderClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        startUpdatingLocation() // (1)Flowから定期的に流れてきたLocationをTextViewに設定する
    }

    override fun onStart() {
        super.onStart()

        lifecycleScope.launch {
            getLastKnownLocation() // (2)非同期でLatest locationを取得して、TextViewに設定する
        }
    }

    private suspend fun getLastKnownLocation() {
        try {
            val lastLocation = fusedLocationClient.awaitLastLocation()
            showLocation(R.id.textView, lastLocation)
        } catch (e: Exception) {
            findAndSetText(R.id.textView, "Unable to get location.")
        }
    }

    private fun startUpdatingLocation() {
        fusedLocationClient.locationFlow()
            .conflate()
            .catch { e ->
                findAndSetText(R.id.textView, "Unable to get location.")
            }
            .asLiveData()
            .observe(this, Observer { location ->
                showLocation(R.id.textView, location)
            })
    }
}
```

まずこの２つの処理

1. Flowから定期的に流れてきたLocationをTextViewに設定する
2. 非同期でLatest locationを取得して、TextViewに設定する

は本質的にLocationをTextViewに設定するロジックなので、無駄をなくすためまとめます。

## 2.まとめて一本のflowにする

Location Flowの初期値にLatest locationを追加するだけで、全部一本のFlowにまとめられます。
Flowには[onStart](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/on-start.html)の拡張関数があるので、初期値を与えるのに使えます。

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var fusedLocationClient: FusedLocationProviderClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
        startUpdatingLocation() // Flowから定期的に流れてきたLocationをTextViewに設定する
    }

    private fun startUpdatingLocation() {
        fusedLocationClient.locationFlow()
            .onStart { offer(fusedLocationClient.awaitLastLocation()) } // ★onStartで初期値を設定する
            .conflate()
            .catch { e ->
                findAndSetText(R.id.textView, "Unable to get location.")
            }
            .asLiveData()
            .observe(this, Observer { location ->
                showLocation(R.id.textView, location)
            })
    }
}
```

ただ厳密に言うと、処理の中身は前から**少し変わります**。

<table>
<thead>
<tr><th></th><th>変更前</th><th>変更後</th></tr>
</thead>
<tbody>
<tr><th>awaitLastLocation</th><td>onStartで発動する</td><td>onCreateで発動する</td></tr>
<tr><th>locationFlow</td><td>onCreateで発動する</td><td>awaitLastLocationが終わったあとに発動する</td></tr>
</tbody>
</table>

前は別々で並列で動かしていたので、`awaitLastLocation`より先に`locationFlow`の値が来ることは原理上可能でしたが、変更後では`awaitLastLocation`より先に`locationFlow`が動くことはないです。

## 3.ViewModelを導入する

この`LiveData`を構築する処理はUIと関係がないので、Activity内で書くべきではありません。

```kotlin
fusedLocationClient.locationFlow()
    .onStart { offer(fusedLocationClient.awaitLastLocation()) }
    .conflate()
    .catch { e -> // ★例外処理が問題
        findAndSetText(R.id.textView, "Unable to get location.")
    }
    .asLiveData()
```

ただ、ここで少し問題があります。中には例外が発生した場合、**エラーメッセージ直接Viewに表示する処理がありました**。
このViewへの依存をなくすために、一番かんたんな手段としては`LiveData`の値を**失敗を表せる型**にすることです。ここでは`Result`を使います（別に他の失敗を表せる型でもOK）。

```kotlin:MainViewModel.kt
class MainViewModel(context: Application) : AndroidViewModel(context) {
    // 本来はこのインスタンスをDIで生成したほうが良いが...
    private val fusedLocationClient = ... // 省略

    val locationLiveData: LiveData<Result<Location>> =
        fusedLocationClient.locationFlow()
            .onStart { emit(fusedLocationClient.awaitLastLocation()) }
            .conflate()
            .map { Result.success(it) }         // 成功した場合はResult.successで包む
            .catch { emit(Result.failure(it)) } // 失敗した場合は例外をResult.failureで包む
            .asLiveData()
}
```

さらにResultで包む処理を隠すために、`Flow<T>.wrapByResult(): Flow<Result<T>>`の拡張関数を別途定義しました。（ソースは[こちら](https://github.com/ibara1454/kotlin-coroutines/blob/178410a2d4acf4f95ab442515ac32f8283a11962/ktx-library-codelab/step-06/myktxlibrary/src/main/java/com/example/android/myktxlibrary/FlowUtils.kt#L30-L33)）

```kotlin
    val locationLiveData: LiveData<Result<Location>> =
        fusedLocationClient.locationFlow()
            .onStart { emit(fusedLocationClient.awaitLastLocation()) }
            .conflate()
            .wrapByResult() // 処理結果をResultで包む
            .asLiveData()
```

UI側は単純に`LiveData`を`observe`するだけでいいです。（ViewModelを使ってるので、画面回転によるflowがもう一回作り直される問題からも開放されました）

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // LiveDataの購読
        viewModel.locationLiveData.observe(this) { result ->
            if (result.isSuccess) {
                showLocation(R.id.textView, location) // 成功した場合はLocationをViewに上書きする
            } else {
                findAndSetText(R.id.textView, "Unable to get location.") // 失敗した場合はエラーメッセージを表示
            }
        }
    }
}
```

## 4.DataBindingを導入する

Data Bindingを使った場合、Viewで利用したい`LiveData`をバインドすれば、明示的に`LiveData#observe`で**購読する必要がなくなります**。

まずData Bindingを使うことを`build.gradle`で設定します。

```kotlin:build.gradle
android {
    ...
    dataBinding {
        enabled = true
    }
}
```

`Result<Location>`型をそのままTextViewに表示できないので、このため`String`を返すように少し変更します。

```kotlin:MainViewModel.kt
class MainViewModel(context: Application) : AndroidViewModel(context) {
    private val fusedLocationClient = ...

    private val locationFlow
        get(): Flow<Result<Location>> =
            fusedLocationClient.locationFlow()
                .onStart { emit(fusedLocationClient.awaitLastLocation()) }
                .conflate()
                .wrapByResult()

    val locationLiveData: LiveData<String> = // ★String型だったら特に変換を噛まさなくてもTextViewにセットできる
        locationFlow
            .map { result ->
                result.fold( // Result<Location> -> Stringを変換するための処理
                    onSuccess = { it.asString(Location.FORMAT_MINUTES) },
                    onFailure = { "Unable to get location. $it" }
                )
            }
            .asLiveData()
}
```

そしてView側は、Layoutで変数を受け取れるようにして、Activityから**変数をバインド**するようにします。

```xml:activity_main.xml
<layout>
    <data>
        <variable
            name="location"
            type="androidx.lifecycle.LiveData&lt;String>" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        ...
    >
        <TextView
            android:text="@{location}"
            ...
        />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```

```kotlin:MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val binding = ActivityMainBinding.inflate(layoutInflater).apply {
            lifecycleOwner = this@MainActivity    // Activityのライフサイクルを渡す
            location = viewModel.locationLiveData // LiveDataを変数locationにバインドする
        }

        setContentView(binding.root)
    }
}
```

こんな感じにViewと関連のないロジックを全部Activityから追い出せました！

アプリの見た目的な変化はないので、修正後のサンプル画像はとくに上げません。

# 完成

実際完成したコードは[GitHub](https://github.com/ibara1454/kotlin-coroutines/tree/178410a2d4acf4f95ab442515ac32f8283a11962/ktx-library-codelab/step-06)にあげています。
（実際のコードと本記事では一部違っている箇所があります。）

変更前との差分は[こちら](https://github.com/ibara1454/kotlin-coroutines/commit/178410a2d4acf4f95ab442515ac32f8283a11962)です。

他に間違い報告、より良い修正方法があればぜひ教えて下さいー