🐷

Activity/FragmentでのFlowのcollect方法

2021/06/23に公開

課題

Activity/FragmentでFlowを使う時、受け取り方がいくつかあるっぽいので整理。
具体的にはMVVMでイベント・状態を受け取る時、今までRxやLiveDataでやっていた部分はどう置き換えられるのか。

実装

launchを使う

一般的にFlowを受け取る時に使うlaunch/collectを使う方法で、DESTROYED になった時は lifecycleScope により自動でキャンセルされる。

リソースを考慮してSTARTED で収集し STOPPED でcollectをキャンセルする場合、つまりバックグラウンドに行った時はFlow出力を処理しない場合には自前で実装する必要がある。

CREATED~DESTROYEDでcollect
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launch{
        flow.collect {
	    // Viewの更新
        }
    }
}
STARTED~STOPPEDでcollect
lateinit var job: Job

override fun onStart() {
    super.onStart()
    job = lifecycleScope.launch{
        flow.collect {
	    // Viewの更新
        }
    }
}

override fun onStop() {
    super.onStop()
    job.cancel()
}

launchWhenXXを使う

lifecycleScope には launchWhenCreated launchWhenStarted launchWhenResumed が生えており、これらは名前の通りライフサイクルが最低XXの時にだけ(例えばSTARTED~STOPPED) 値が受け付けられる。

STOPPED になったら一時停止され、 STARTED でおなじジョブから再開される。

launchWhenStarted
lateinit var job: Job
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launchWhenStarted {
        flow.collect {
	    // Viewの更新
        }
    }
}

repeatOnLifecycle

lifecycle-runtime-ktx:2.4.0-alpha01 で導入されたLifecycle.repeatOnLifecycle を使う方法。
指定されたライフサイクルの状態で値が受け付けられる。
launchWhenStarted はSTOPPEDになったら一時停止されSTARTEDで再開されるのに対し、 repeatOnLifecycle ではSTOPPEDになったら一旦ジョブがキャンセルされ、STARTEDでは新しく開始される。

複数のflowを同時に受け付ける時(例えば画面状態を表すUiStateのFlowと、Toastなど単発のイベントを受け取るFlow)、 collect はsuspendなため並列で受け取るためにそれぞれを別のcoroutineScopeで実行するためにlaunchでくくる必要がある。

repeatOnLifecycle
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launch {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collect {
	        // Viewの更新
            }
        }
    }
    
    lifecycleScope.launch {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
	    launch {
                flow.collect {
	            // Viewの更新
                }
	    }
	    launch {
                flow.collect {
	            // Viewの更新
                }
	    }
        }
    }
}

flowWithLifecycle

こちらもlifecycle-runtime-ktx:2.4.0-alpha01 で導入されたFlow.flowWithLifecycle を使う。
上記のrepeatOnLifecycleの書き換えになっている。
こちらのほうがネストが一段少なくなる。

Flowのextensionで生えており、 mapflowOn と同じノリで、ライフサイクルを制限するように使われるようだ。

flowWithLifecycle
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    lifecycleScope.launch {
        flow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
	    .collect {
	    	// Viewの更新
	    }
    }
}

addRepeatingJob

lifecycle-runtime-ktx:2.4.0-alpha01 で上記と合わせて導入されたLifecycleOwner.addRepeatingJob を使う。が、2.4.0-alpha01 で削除された
最新では使えないので以下参考までに。

addRepeatingJob は今までの書き方と比べて launch{} の部分もやってくれる。更にネストが減って嬉しい。

またこれまでの方法と比べるとflowを複数collectする場合にはそれぞれでlaunchをしなければいけなかったのに対し、こちらは addRepeatingJob を複数回書けば良い。

実際 flowWithLifecycle のドキュメントに、複数回collectする場合はこちらを使うべき、と書いてあった。

addRepeatingJob
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    addRepeatingJob(Lifecycle.State.STARTED) {
        flow.collect {
	    // Viewの更新
        }
    }
}

結論

いくつか書き方を示したが基本的に repeatOnLifecycle を使うで統一するのが良いかと思う。

理由として

  • 特別な理由がない限りはリソース的に launchWhenXX より repeatOnLifecycle が良い
  • repeatOnLifecycleflowWithLifecycle 同じことができるが、前者のほうが複数flowを受け付ける場合に推奨されている

が挙げられる。

flowWithLifecycle はメソッドチェーンで処理を連結したい場合、 launchWhenXXonStop -> onStart など一旦状態が落ちて戻ってきた時にFlowをキャンセルしたくないといった場合に使う。

Discussion