[Kotlin] MutableStateFlowをStateFlowに変換するときasStateFlowを使うべきか?
asStateFlow
は使うべきなのか?
asStateFlow
はMutableStateFlow
をStateFlow
に変換する拡張関数なのですが、Android公式ドキュメントにはasStateFlow
を利用しないサンプルコードが記載されていたり、Kotlin公式ドキュメントにはasStateFlow
を利用するサンプルコードが記載されていたりと、結局のところasStateFlow
を使うべきかどうかが曖昧な状態になっています。
fun <T> MutableStateFlow<T>.asStateFlow(): StateFlow<T>
Android公式のサンプルコード
-
MutableStateFlow
(_uiStateFlow
)をStateFlow
(uiState
)に変換するときは型キャストを利用している
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState
︙ 省略
}
Kotlin公式のサンプルコード
-
MutableStateFlow
(_counter
)をStateFlow
(counter
)に変換するときはasStateFlow
を利用している
class CounterModel {
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter = _counter.asStateFlow() // publicly exposed as read-only state flow
︙ 省略
}
asStateFlow
を使うべきかどうか調べてみた
という感じでAndroidやKotlinの公式ドキュメントでは言っていることが違うのでasStateFlow
を使うべきかどうかを自分なりに調べてみました。結論を先にいうとasStateFlow
を利用したほうが、防御プログラミングの観点で安全なコードを記述できるので使ったほうが良いらしいです。
class CounterModel {
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter = _counter.asStateFlow() // publicly exposed as read-only state flow
︙ 省略
}
MutableStateFlow
に無理やりキャストされる問題を防げる
参照側で以下のようなMutableStateFlow
を保持するStateHolder
があったとする。本来はStateHolder
のupdate()
を通してのみMutableStateFlow
の値の更新が可能なのですが、asStateFlow
を使っていないためStateHolder
を参照している側でMutableStateFlow
に無理やりキャストできStateHolder
のupdate
を経由せずに更新が可能になってしまう。
asStateFlowを利用しないと無理やりキャストできてしまう
class StateHolder {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _stateFlow : MutableStateFlow<Int> = MutableStateFlow(0)
val stateFlow: StateFlow<Int> = _stateFlow
fun update(number: Int) {
scope.launch { _stateFlow.emit(number) }
}
}
fun main() {
val stateHolder = StateHolder()
stateHolder.update(1)
// 無理やりキャストできてしまう
val mutableStateFlow = stateHolder.stateFlow as MutableStateFlow
runBlocking { mutableStateFlow.emit(2) }
}
asStateFlow
を使うことで内部的にReadonlyStateFlow
が生成されるようになり、StateHolder
を参照する側でMutableStateFlow
へキャストしたときにjava.lang.ClassCastException
が発生するようになるため、無理やりキャストするお行儀の悪いコードを許さないように対策ができるようになります。
asStateFlowを利用すると無理やりキャストできなくなる
class StateHolder {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _stateFlow : MutableStateFlow<Int> = MutableStateFlow(0)
val stateFlow: StateFlow<Int> = _stateFlow.asStateFlow()
fun update(number: Int) {
scope.launch { _stateFlow.emit(number) }
}
}
fun main() {
val stateHolder = StateHolder()
stateHolder.update(1)
// java.lang.ClassCastExceptionが発生するのでこの後の処理は実行されない
val mutableStateFlow = stateHolder.stateFlow as MutableStateFlow
runBlocking { mutableStateFlow.emit(2) }
}
MutableStateFlow
に無理やりキャストしなければ良い話では?
参照側でそもそも無理やりキャストするのが悪いのではと感じるのですが、バリデーションをすり抜けて処理を実行できるケースを作ってしまい、特にSDKやライブラリのコードにこれらが含まれると攻撃に使われてしまう可能性があるので、できるだけasStateFlow
を利用するのが良いみたいです。
-
Repository
でMutableStateFlow
とStateFlow
を定義してStateFlow
を共有する -
Repository
のupdate()
でMutableStateFlow
の値を更新可能にする -
Repository
のupdate()
ではバリデーションを実施し、MutableStateFlow
には整数値のみ更新されるように保証する -
Repository
でMutableStateFlow
を購読し、値が更新されたらFileWriter
を利用して、更新された値をファイルに永続化する
class Repository {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _stateFlow : MutableStateFlow<Int> = MutableStateFlow(0)
val stateFlow: StateFlow<Int> = _stateFlow
init {
scope.launch {
stateFlow.collectLatest { value ->
FileWriter.write(value)
}
}
}
fun update(number: Int) {
if (number < 0) InvalidParameterException("Invalid number")
scope.launch { _stateFlow.emit(number) }
}
}
fun main() {
val repository = Repository()
val mutableStateFlow = repository.stateFlow as MutableStateFlow
// バリデーションをすり抜けるので、無効値の書き込み処理が実行されてしまう
runBlocking { mutableStateFlow.emit(-1) }
}
asSharedFlowも同じく使ったほうが良さそう
ちなみにasSharedFlow
はMutableSharedFlow
をSharedFlow
に変換する拡張関数もあります。
public fun <T> MutableSharedFlow<T>.asSharedFlow(): SharedFlow<T> = ReadonlySharedFlow(this, null)
MutableSharedFlow
でもMutableStateFlow
と同様の問題が発生するためasSharedFlow
も積極的に利用するのが良さそうです。
asSharedFlowを利用しないと無理やりキャストできてしまう
class StateHolder {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _sharedFlow : MutableSharedFlow<Int> = MutableSharedFlow()
val sharedFlow: SharedFlow<Int> = _sharedFlow
fun update(number: Int) {
scope.launch { _sharedFlow.emit(number) }
}
}
fun main() {
val stateHolder = StateHolder()
stateHolder.update(1)
// 無理やりキャストできてしまう
val mutableSharedFlow = stateHolder.sharedFlow as MutableSharedFlow
runBlocking { mutableSharedFlow.emit(2) }
}
asSharedFlowを利用すると無理やりキャストできなくなる
class StateHolder {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val _sharedFlow : MutableSharedFlow<Int> = MutableSharedFlow()
val sharedFlow: SharedFlow<Int> = _sharedFlow.asSharedFlow()
fun update(number: Int) {
scope.launch { _sharedFlow.emit(number) }
}
}
fun main() {
val stateHolder = StateHolder()
stateHolder.update(1)
// java.lang.ClassCastExceptionが発生するのでこの後の処理は実行されない
val mutableSharedFlow = stateHolder.sharedFlow as MutableSharedFlow
runBlocking { mutableSharedFlow.emit(2) }
}
Discussion