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する
サーバーからデータを取得した場合の処理の流れは、以下の通りです。
- APIコール
- APIのModelをドメインモデルに変換
- ドメインモデルをDBに保存
- (ここまでうまくいったら)サーバーからの取得時刻として現在時刻を保存しておく
- ドメインモデル返却
これを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}」