Androidアプリ開発において、Lifecycleを考慮することでユーザのリソースを有効的に利用することが出来ます。
ユーザのリソースを不要に消費しないようにするためには、必要になったときに必要な処理を行うことが重要です。
バックグラウンド時や他画面を開いているときAPIを叩く必要性が出てきても、LifecycleがSTARTED
になるまで待つべきでしょう。
今回は、Kotlin Coroutinesを用いて、LifecycleがSTARTED
、すなわちフォアグラウンドになったときにAPI等を叩くような処理について考えてみます。
問題になるようなケースについて紹介をします。
例えば、課金することで利用可能なコンテンツが増えるようなアプリケーションを考えたとき、課金した後にAPIを叩き直すケースは多くあるでしょう。
複数画面が重なった状態で、課金されたタイミングで全ての画面に更新をかけた場合、APIが大量に叩かれ、ユーザのリソースも消費しますし、サーバの負荷も心配になります。
こういったケースでは、その画面に戻った際にAPIを叩き直すのが良いでしょう。
その他、ログインしたタイミングや、なにかの操作を行った後等、APIを呼び直したい状況はいくつか考えられると思います。
上記を解決するために、StateFlow
を使ったActiveBlocker
を用意します。
class ActiveBlocker {
private val isActive = MutableStateFlow(false)
fun activate() {
isActive.value = true
}
fun deactivate() {
isActive.value = false
}
suspend fun waitUntilActive() {
if (isActive.value) return
isActive.filter { it }.first()
}
}
isActive
がtrue
だったら即座にreturn
し、false
だったら次にtrue
になるまでfirst
を使って待っています。
これをlifecycleのonStart
でisActive
を true
に、onStop
でfalse
にします。LifecycleObserverを使うのが良いでしょう。
val activeBlocker = ActiveBlocker()
val lifecycleObserver: LifecycleObserver = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
activeBlocker.activate()
}
override fun onStop(owner: LifecycleOwner) {
activeBlocker.deactivate()
}
}
あとはactiveになるまで待ってほしいところで waitUntilActive
を呼びます。
fun doSomething() {
viewModelScope.launch {
activeBlocker.waitUntilActive()
api.call()
}
}
これにより、isActive
が true
のときは即座にAPIを呼んでくれますし、false
の場合は true
になるまで待ってapiを呼び出してくれます。
イベントをFlowで流して、それをハンドリングすることはよくあるでしょう。
Flowに対してActiveBlocker
を使う際はいくつか注意が必要です。
以下のように書いてしまうと、deactiveの場合にFlowの流れを止めてしまい、bufferを消費していきます。
val activeBlocker = ActiveBlocker()
eventFlow.onEach {
activeBlocker.waitUntilActive()
api.call()
}.launchIn(viewModelScope)
また、activeになったタイミングでdeactiveで流れてきたイベントの回数分(bufferの数分)API callが行われます。
多くの場合、最新のイベント1件が取得できれば良いと思うので、以下のような拡張関数を用意します。
fun <T> Flow<T>.waitUntilActive(blocker: ActiveBlocker): Flow<T> {
val flow = this
var job: Job? = null
return channelFlow {
flow.collect {
job?.cancel()
job = launch {
blocker.waitUntilActive()
send(it)
}
}
}
}
waitUntilActive
の後に値を送信するCoroutinesをlaunch
します。新しい値が流れてきたときは以前のCoroutinesをcancel
して新しくlaunch
し直しています。
このように書くことで、buffer
を消費せず、最新の1件のみを扱うことが出来ます。
val activeBlocker = ActiveBlocker()
eventFlow
.waitUntilActive(activeBlocker)
.onEach { api.call() }
.launchIn(viewModelScope)
ちなみにconflateを使って以下のように書いた場合は、waitUntilActive
で待機していたものと、conflate
で待機していたものの、合計2回分処理されてしまうので注意してください。
val activeBlocker = ActiveBlocker()
eventFlow.conflate().onEach {
activeBlocker.waitUntilActive()
api.call()
}.launchIn(viewModelScope)
今回はKotlin Coroutinesの活用例について紹介しました。
ActiveBlockerはLifecycleに直接依存しないようにしており、Lifecycle以外のイベントを使ったり、ロジックレイヤー等でも使いやすくなっています。
ぜひ効果的な活用方法を見つけて見てください。
Kotlin Coroutinesの解説本をZennにて販売しています。より詳しく学びたい方は、こちらも合わせて確認してみて下さい。