この記事はAndroid Advent Calendar 2020 の2日目の記事です。
SharedFlowやStateFlowの登場により、ますますkotlin coroutinesを手軽に扱えるようになってきました。
AndroidのMVVMにおいても、LiveDataの代わりにStateFlowを使ってViewとViewModelをbindingすることが可能になりました。
一方で、ViewModelからViewに状態ではなくイベントを送るのは未だいくつかの問題があります。
今回は、複数の方法をメリットデメリットとともに紹介したいと思います。
イベントを扱う一番簡単な方法は SharedFlow
を使う方法です。
以下のようにMutableSharedFlowを使って、 onEvent()
が呼ばれたタイミングでflowに値を流すことができます。
class SampleViewModel : ViewModel() {
private val _event = MutableSharedFlow<Int>()
val event: SharedFlow<Int> get() = _event
fun onEvent() {
viewModelScope.launch {
_event.emit(1)
}
}
}
公開するときは SharedFlow
か Flow
にcastして外から変更できないようにしておいたほうが良いでしょう。
Activity側ではこのようにsubscribeします。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.event.collect {
Log.d("SampleActivity", it.toString())
}
}
}
lifecycleScope を使うことで onDestory
時に自動的にsubscribeを解除してくれ、リークする心配がありません。
Startからstopの間のみイベントを流す場合は launchWhenStarted
を使ってください。(それ以外のときはpause状態になります)
lifecycleScope.launchWhenStarted {
viewModel.event.collect {
Log.d("SampleActivity", it.toString())
}
}
lifecycle-runtime-ktx:2.4.0-alpha01でrepeatOnLifecycleが追加されました。
launchWhenStarted
は削除される計画もあるため、repeatOnLifecycle
を使うことをおすすめします。
基本的にはこれで動作しますが、一つ問題があります。
onCreateより前や画面回転中等、subscriberがない間に発生したイベントは、どこにも届かず破棄されてしまいます。
class SampleViewModel : ViewModel() {
private val _event = MutableSharedFlow<Int>()
val event: SharedFlow<Int> get() = _event
init {
viewModelScope.launch {
// onStartより前なのでこのタイミングのイベントはviewに届かない
_event.emit(1)
}
}
}
Channelを使うことで、先程問題になったのsubscriberがない間のイベントも保持することができます。
Channelは作成時にbuffer sizeを指定することができるので、状況に合わせて設定しましょう。
UNLIMITED
を設定することもできます。
private val _event = Channel<Int>(Channel.UNLIMITED)
val event: ReceiveChannel<Int> get() = _event
init {
viewModelScope.launch {
_event.send(1)
_event.send(2)
_event.send(3)
}
}
公開は ReceiveChannelですることで、外からの書き換えはできなくなります。
view側ではこのようにsubscribeします。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenStarted {
viewModel.event.receiveAsFlow().collect {
Log.d("SampleActivity", it.toString())
}
}
}
receiveAsFlowを使ってFlowに変換し、collectすると、subscribeされてない時のデータも受け取ることができます。
D/SampleActivity: 1
D/SampleActivity: 2
D/SampleActivity: 3
この解決方法の問題点は、複数箇所でsubscribeすることができません。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenStarted {
viewModel.event.receiveAsFlow().collect {
Log.d("SampleActivity", "collect1:$it")
}
}
lifecycleScope.launchWhenStarted {
viewModel.event.receiveAsFlow().collect {
// 上でreceiveされてるため、ここでデータは受け取れない
Log.d("SampleActivity", "collect1:$it")
}
}
}
複数箇所でsubscribe可能で、イベントの漏れが発生しない方法として、ViewModel側にlifecycleを渡し、ViewModel側でバッファリングする方法があると思います。
以下のような、 LifecycleStateFlow
というクラスと bufferUntilStarted
というFlowの拡張関数を用意します。
class LifecycleStateFlow private constructor(
private val mutableStateFlow: MutableStateFlow<Lifecycle.State>
) : LifecycleEventObserver, StateFlow<Lifecycle.State> by mutableStateFlow {
constructor() : this(MutableStateFlow(Lifecycle.State.INITIALIZED))
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
mutableStateFlow.value = source.lifecycle.currentState
}
suspend fun waitUntilAtLeast(state: Lifecycle.State) {
if (value.isAtLeast(state)) return
first { it.isAtLeast(state) }
}
}
fun <T> Flow<T>.bufferUntilAtLeast(
lifecycleStateFlow: LifecycleStateFlow,
state: Lifecycle.State,
capacity: Int = Channel.BUFFERED
): Flow<T> {
return buffer(capacity)
.onEach { lifecycleStateFlow.waitUntilAtLeast(state) }
}
fun <T> Flow<T>.bufferUntilStarted(
lifecycleStateFlow: LifecycleStateFlow,
capacity: Int = Channel.BUFFERED
): Flow<T> {
return bufferUntilAtLeast(
lifecycleStateFlow,
Lifecycle.State.STARTED,
capacity
)
}
ViewModelはこのように使います。
class SampleViewModel : ViewModel() {
val lifecycleStateFlow = LifecycleStateFlow()
private val _event = MutableSharedFlow<Int>()
val event: SharedFlow<Int> = _event
.bufferUntilStarted(lifecycleStateFlow)
.shareIn(viewModelScope, SharingStarted.Eagerly)
init {
viewModelScope.launch {
_event.emit(1)
_event.emit(2)
_event.emit(3)
}
}
}
特定のflowに対して、bufferUntilStarted
をつけることで、Startedになるまでバッファリングしてくれます。
shareInで再度hot stream化することを忘れないでください。
View側ではこのように利用します。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel.lifecycleStateFlow)
lifecycleScope.launch {
viewModel.event.collect {
Log.d("SampleActivity", "collect1:$it")
}
}
lifecycleScope.launch {
viewModel.event.collect {
Log.d("SampleActivity", "collect2:$it")
}
}
}
lifecycle.addObserver
で lifecycleStateFlow
を指定することを忘れないでください。
また、今回はViewModel側でバッファリングしてるため、 launchWhenStarted
ではなく通常の launch
で良いでしょう。
このように2つsubscriberがあっても、2つともに全ての値が流れてきます。
onEach1:1
onEach1:2
onEach1:3
onEach2:1
onEach2:2
onEach2:3
若干複雑なのと、ViewModelがlifecycleを知っていることに違和感がある人もいるかも知れませんが、全てのデータが通知され、複数箇所でsubscribe可能なイベントを作成することができました。
今回はkotlin coroutinesを使ってViewにイベントを通知する方法について複数紹介しました。
少し複雑ですが、全てのデータが通知され、複数箇所でsubscribe可能なイベントも作成することができました。
データの流れてくるタイミング、データを欠損したくないか否か、複数subscribeする必要があるか等で解決方法が変わってくると思います。
また、SharedFlowもChannelも buffer
や onBufferOverflow
等、オプションによって挙動を変えることができるので、ぜひ色々試してみてください。
SharedFlowの深堀り、replay, bufferって何【kotlin coroutines flow】
また、MVVMのViewModelは本来状態を扱うべきであり、イベントでやり取りするのは最小限であるべきだと考えています。
イベントのご利用は計画的に。
- 2021/01/31追記-
Kotlin Coroutinesの解説本をZennにて販売しています。より詳しく学びたい方は、こちらも合わせて確認してみて下さい。