vol.2/2 APIのGET結果をキャッシュする (FlowChain,例外処理編)

# APIのGET結果をキャッシュしたい！
[前回](https://qiita.com/jozuko_dev/items/e1f47bd1a14a138fb51f)Roomの導入を行いました。
今回は、以下の処理をFlowのChainを使って記述していきます。
- 10分経過している時は、[サーバーから取得]->[DB保存]->[結果返却]
- 未経過の場合は、[DBから取得]->[結果返却]

## 各処理のFlow
サーバーからの取得と、DBへの保存、DBからの取得は、すべて異なるflowで記述しています。
```kotlin
// サーバーからの取得
flow<Future<AreaApiModel>> {
    val response = forecastApi.getArea()
    if (response.isSuccessful) {
        emit(Future.Success(value = response.body()!!))
    } else {
        throw HttpException(response)
    }
}.catch { cause ->
    emit(Future.Error(cause))
}.onStart {
    emit(Future.Proceeding)
}.flowOn(Dispatchers.IO)
```
```kotlin
// DBへの保存
flow<Future<Area>> {
    db.withTransaction {
        /* データベース操作 */
    }
    // サーバーからの取得時間を保存
    sharedPref.areaDataUpdatedAt = System.currentTimeMillis()
    emit(Future.Success(area))
}.catch { cause ->
    emit(Future.Error(cause))
}.flowOn(Dispatchers.IO)
```
```kotlin
// DBからの取得
flow<Future<Area>> {
    val areaEntityMap = areaDao.getAll()
    val area = AreaAdapter.adaptFromDb(areaEntityMap)
    emit(Future.Success(area))
}.catch { cause ->
    emit(Future.Error(cause))
}.flowOn(Dispatchers.IO)
```

## サーバーからの取得をflow-chainする
サーバーからデータを取得した場合の処理の流れは、以下の通りです。
1. APIコール
1. APIのModelをドメインモデルに変換
1. ドメインモデルをDBに保存
1. （ここまでうまくいったら）サーバーからの取得時刻として現在時刻を保存しておく
1. ドメインモデル返却

これをflow-chainで書いていきます。
```kotlin
@OptIn(ExperimentalCoroutinesApi::class)
private fun getAreaFromServer(): Flow<Future<Area>> {
    // 1. APIコール
    return flow<Future<AreaApiModel>> {
        val response = forecastApi.getArea()
        if (response.isSuccessful) {
            emit(Future.Success(value = response.body()!!))
        } else {
            throw HttpException(response)
        }
    // 2. APIのModelをドメインモデルに変換
    }.map { apiModelFuture: Future<AreaApiModel> ->
        when (apiModelFuture) {
            is Future.Error -> Future.Error(apiModelFuture.error)
            is Future.Success -> Future.Success(AreaAdapter.adaptFromApi(apiModelFuture.value))
            is Future.Proceeding -> Future.Proceeding
            is Future.Idle -> Future.Proceeding
        }
    // 3. ドメインモデルをDBに保存
    }.flatMapConcat { areaFuture: Future<Area> ->
        if (areaFuture is Future.Success) {
            flow<Future<Area>> {
                db.withTransaction {
                    /* DBに保存 */
                }
                // 4. サーバーからの取得時間を保存
                sharedPref.areaDataUpdatedAt = System.currentTimeMillis()
                // 5. ドメインモデル返却
                emit(Future.Success(areaFuture.value))
            }.catch { cause ->
                emit(Future.Error(cause))
            }
        } else {
            // 5. ドメインモデル返却(APIコールがSuccessでないときは、そのまま返却する)
            flowOf(areaFuture)
        }
    }.catch { cause ->
        emit(Future.Error(cause))
    }
}
```
### 1. APIコール
forecastApi.getArea()を使用して、AreaApiModelを取得します。
特にひねりもないですね。

### 2. APIのModelをドメインモデルに変換
Adapterを使ってAreaApiModelをAreaに変換します。
型変換の部分はflowではないので、「.map{}」でchainします。
inputのクラスは"Future&lt;AreaApiModel&gt;"で、outputは"Future&lt;Area&gt;"です。

### 3. ドメインモデルをDBに保存
DBに保存するところはFlowで作りましたので、「.flatMapConcat{}」でchainします。

### 4. サーバーからの取得時間を保存
SharedPreferencesに、サーバーからの取得時間として、現在時刻を保存しています。
あとで、10分経過しているかを見るときに使用します。

### 5. ドメインモデル返却
ここまでで生成したドメインをemitしています。

## DBからの取得
「各処理のFlow」で記載したflowを関数化しただけです。
```kotlin
private fun getAreaFromLocal(): Flow<Future<Area>> {
    return flow<Future<Area>> {
        val areaMap = areaDao.getAll()
        val area = AreaAdapter.adaptFromDb(areaMap)
        emit(Future.Success(area))
    }.catch { cause ->
        emit(Future.Error(cause))
    }
}
```

## サーバーからとるか、ローカルからとるかをflow化する
サーバーからの前回の取得時刻を元に、サーバーデータ/ローカルデータのどちらかを使ってドメインを返却するflowのchainを記述します。

```kotlin
@OptIn(ExperimentalCoroutinesApi::class)
override fun getArea(): Flow<Future<Area>> {
    return flow {
        val areaDataUpdatedAt = sharedPref.areaDataUpdatedAt
        emit(areaDataUpdatedAt.isPassedTime(minutes = 10))
    }.flatMapConcat { needServerData ->
        if (needServerData) {
            getAreaFromServer()
        } else {
            getAreaFromLocal()
        }
    }.onStart {
        emit(Future.Proceeding)
    }.catch { cause ->
        emit(Future.Error(cause))
    }.flowOn(dispatchers)
}
```
「Long#isPassedTime」は、自作の関数です。気になる方は、[GitHub](https://github.com/jozuko/WeatherForecast/blob/e108866d1d52be250ee2d7ac3ba57515e37fefb4/app/src/main/java/com/jozu/weatherforecast/domain/extension/LongEx.kt)を見てください。

:::note info
<B>SharedPreferencesのデータ処理</B>
SharedPreferencesからのデータ取得は、キャッシュデータの取得なので時間はかからないです。そのため、flow化する必要はないのですが、お勉強のためにflow内で処理しています。
書き込みの方は、apply()ではなくcommit()で行うと、DiskI/Oが発生しますので、flow内でやることをおすすめします。
:::

## 完成！

完成コードは[こちら](https://github.com/jozuko/WeatherForecast/blob/e108866d1d52be250ee2d7ac3ba57515e37fefb4/app/src/main/java/com/jozu/weatherforecast/infrastructure/repository/AreaRepositoryImpl.kt)です。

# （番外編）Flowの例外処理
「.flatMapConcat{}」でつなぐ際には、例外処理に気をつけていきましょう。

## chainしていないflowで、例外があったとき
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        delay(10L)
        throw IllegalArgumentException("1")
    }.onStart {
        // ② onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ③ catch
        emit(FlowFuture.Error(it))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(1)」
処理の流れ：「② onStart」->「① flow」->「③ catch」

## flow{}.map{}のflow{}で、例外があったとき
「② map」には入りません
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        throw IllegalArgumentException("1")
    }.map {
        // ② map
        it
    }.onStart {
        // ③ onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ④ catch
        emit(FlowFuture.Error(it))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(1)」
処理の流れ：「③ onStart」->「① flow」->「④ catch」

## flow{}.map{}のmap{}で、例外があったとき
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        delay(10L)
        emit(FlowFuture.Success("1"))
    }.map {
        // ② map
        throw IllegalArgumentException("2")
        it
    }.onStart {
        // ③ onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ④ catch
        emit(FlowFuture.Error(it))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(2)」
処理の流れ：「③ onStart」->「① flow」->「② map」->「④ catch」

## flow{}.map{1}.map{2}のmap{1}で、例外があったとき
「③ map2」には入りません
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        delay(10L)
        emit(FlowFuture.Success("1"))
    }.map {
        // ② map1
        throw IllegalArgumentException("2")
        it
    }.map {
        // ③ map2
        it
    }.onStart {
        // ④ onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ⑤ catch
        emit(FlowFuture.Error(it))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(2)」
処理の流れ：「④ onStart」->「① flow」->「② map」->「⑤ catch」

## flow{}.flatMapConcat{}のflow{}で例外があったとき
「② flatMapConcat{flow}」は実行されない
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        delay(10L)
        throw IllegalArgumentException("1")
    }.flatMapConcat {
        flow {
            // ② flatMapConcat{flow}
            delay(10L)
            emit(it)
        }
    }.onStart {
        // ③ onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ④ catch
        emit(FlowFuture.Error(it))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(1)」
処理の流れ：「③ onStart」->「① flow」->「④ catch」

## flow{}.flatMapConcat{}のcatchしてないflatMapConcat{flow{}}で例外があったとき
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        delay(10L)
        emit(FlowFuture.Success("1"))
    }.flatMapConcat {
        flow<FlowFuture<String>> {
            // ② flatMapConcat{flow}
            delay(10L)
            throw IllegalArgumentException("2")
            emit(FlowFuture.Success("2"))
        }
    }.onStart {
        // ③ onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ④ catch
        emit(FlowFuture.Error(it))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(2)」
処理の流れ：「③ onStart」->「① flow」->「② flatMapConcat{flow}」->「④ catch」

## flow{}.flatMapConcat{1}.flatMapConcat{2}のcatchしてないflatMapConcat{1 flow{}}で例外があったとき
「③ flatMapConcat{2 flow}」は実行されない
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        delay(10L)
        emit(FlowFuture.Success("1"))
    }.flatMapConcat {
        flow<FlowFuture<String>> {
            // ② flatMapConcat{1 flow}
            delay(10L)
            throw IllegalArgumentException("2")
            emit(FlowFuture.Success("2"))
        }
    }.flatMapConcat {
        flow<FlowFuture<String>> {
            // ③ flatMapConcat{2 flow}
            delay(10L)
            emit(FlowFuture.Success("3"))
        }
    }.onStart {
        // ④ onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ⑤ catch
        emit(FlowFuture.Error(it))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(2)」
処理の流れ：「④ onStart」->「① flow」->「② flatMapConcat{1 flow}」->「④ catch」

## flow{}.flatMapConcat{}のcatchしてるflatMapConcat{flow{}}で例外があったとき
「⑤ catch」には入りません
```kotlin
fun flowTest(): Flow<FlowFuture<String>> {
    return flow<FlowFuture<String>> {
        // ① flow
        delay(10L)
        emit(FlowFuture.Success("1"))
    }.flatMapConcat {
        flow<FlowFuture<String>> {
            // ② flatMapConcat{flow}
            delay(10L)
            throw IllegalArgumentException("2")
            emit(FlowFuture.Success("2"))
        }.catch {
            // ③ flatMapConcat{ｃａｔｃｈ}
            emit(FlowFuture.Error(Throwable("2-${it.localizedMessage}")))
        }
    }.onStart {
        // ④ onStart
        emit(FlowFuture.Proceeding("1"))
    }.catch {
        // ⑤ catch
        emit(FlowFuture.Error(Throwable("1-${it.localizedMessage}")))
    }.flowOn(Dispatchers.IO)
}
```
出力：「Proceeding(1)」->「Error(2-2)」
処理の流れ：「④ onStart」->「① flow」->「② flatMapConcat{flow}」->「③ flatMapConcat{ｃａｔｃｈ}」