[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