🐻‍❄️

[Kotlin] MutableStateFlowをStateFlowに変換するときasStateFlowを使うべきか?

2023/06/11に公開

asStateFlowは使うべきなのか?

asStateFlowMutableStateFlowStateFlowに変換する拡張関数なのですが、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
        ︙ 省略
}

https://developer.android.com/kotlin/flow/stateflow-and-sharedflow?hl=ja

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
        ︙ 省略
}

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-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があったとする。本来はStateHolderupdate()を通してのみMutableStateFlowの値の更新が可能なのですが、asStateFlowを使っていないためStateHolderを参照している側でMutableStateFlowに無理やりキャストできStateHolderupdateを経由せずに更新が可能になってしまう。

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を利用するのが良いみたいです。

  • RepositoryMutableStateFlowStateFlowを定義してStateFlowを共有する
  • Repositoryupdate()MutableStateFlowの値を更新可能にする
  • Repositoryupdate()ではバリデーションを実施し、MutableStateFlowには整数値のみ更新されるように保証する
  • RepositoryMutableStateFlowを購読し、値が更新されたら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も同じく使ったほうが良さそう

ちなみにasSharedFlowMutableSharedFlowSharedFlowに変換する拡張関数もあります。

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