Blog

Kotlin Coroutinesでフォアグラウンドになるまで待つ【Android】

Androidアプリ開発において、Lifecycleを考慮することでユーザのリソースを有効的に利用することが出来ます。

ユーザのリソースを不要に消費しないようにするためには、必要になったときに必要な処理を行うことが重要です。

バックグラウンド時や他画面を開いているときAPIを叩く必要性が出てきても、LifecycleがSTARTEDになるまで待つべきでしょう。

今回は、Kotlin Coroutinesを用いて、LifecycleがSTARTED、すなわちフォアグラウンドになったときにAPI等を叩くような処理について考えてみます。

具体例:課金後にAPIを叩きたい

問題になるようなケースについて紹介をします。

例えば、課金することで利用可能なコンテンツが増えるようなアプリケーションを考えたとき、課金した後にAPIを叩き直すケースは多くあるでしょう。

複数画面が重なった状態で、課金されたタイミングで全ての画面に更新をかけた場合、APIが大量に叩かれ、ユーザのリソースも消費しますし、サーバの負荷も心配になります。

こういったケースでは、その画面に戻った際にAPIを叩き直すのが良いでしょう。

その他、ログインしたタイミングや、なにかの操作を行った後等、APIを呼び直したい状況はいくつか考えられると思います。

ActiveBlocker

上記を解決するために、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()
    }
}

isActivetrueだったら即座にreturnし、falseだったら次にtrueになるまでfirstを使って待っています。

これをlifecycleのonStartisActivetrueに、onStopfalseにします。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()
    }
}

これにより、isActivetrue のときは即座にAPIを呼んでくれますし、false の場合は true になるまで待ってapiを呼び出してくれます。

Flowのイベントハンドリング

イベントを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にて販売しています。より詳しく学びたい方は、こちらも合わせて確認してみて下さい。

詳解 Kotlin Coroutines [2021] | Zenn

人気の記事

kotlin coroutinesのFlow, SharedFlow, StateFlowを整理する

Jetpack ComposeとKotlin Coroutinesを連携させる

Layout Composableを使って複雑なレイアウトを組む【Jetpack Compose】

テスト用Dispatcherの使い分け【Kotlin Coroutines】

Flow.combineの内部実装がすごい話

Jetpack ComposeのRippleエフェクトを深堀り、カスタマイズも