0
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-07-20

APIのGET結果をキャッシュしたい!

前回Roomの導入を行いました。
今回は、以下の処理をFlowのChainを使って記述していきます。

  • 10分経過している時は、[サーバーから取得]->[DB保存]->[結果返却]
  • 未経過の場合は、[DBから取得]->[結果返却]

各処理のFlow

サーバーからの取得と、DBへの保存、DBからの取得は、すべて異なるflowで記述しています。

// サーバーからの取得
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)
// DBへの保存
flow<Future<Area>> {
    db.withTransaction {
        /* データベース操作 */
    }
    // サーバーからの取得時間を保存
    sharedPref.areaDataUpdatedAt = System.currentTimeMillis()
    emit(Future.Success(area))
}.catch { cause ->
    emit(Future.Error(cause))
}.flowOn(Dispatchers.IO)
// 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コール
  2. APIのModelをドメインモデルに変換
  3. ドメインモデルをDBに保存
  4. (ここまでうまくいったら)サーバーからの取得時刻として現在時刻を保存しておく
  5. ドメインモデル返却

これをflow-chainで書いていきます。

@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<AreaApiModel>"で、outputは"Future<Area>"です。

3. ドメインモデルをDBに保存

DBに保存するところはFlowで作りましたので、「.flatMapConcat{}」でchainします。

4. サーバーからの取得時間を保存

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

5. ドメインモデル返却

ここまでで生成したドメインをemitしています。

DBからの取得

「各処理のFlow」で記載したflowを関数化しただけです。

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を記述します。

@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を見てください。

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

完成!

完成コードはこちらです。

(番外編)Flowの例外処理

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

chainしていないflowで、例外があったとき

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」には入りません

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{}で、例外があったとき

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」には入りません

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}」は実行されない

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{}}で例外があったとき

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}」は実行されない

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」には入りません

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{catch}
            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{catch}」

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0