uiState を宣言的に組み立てる Flow オペレーター ベスト10

みんなはいくつ使っていますか。

 

🧑🏻‍💻 1. stateIn: Flow を StateFlow に変換する

宣言的な uiState の締めくくりです。WhileSubscribed(5_000) にしておくと、画面回転では再購読せず、バックグラウンドでは上流を止められます。


val uiState: StateFlow<UiState> = flow
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = UiState.Loading
    )

 

🧑🏻‍💻 2. combine: 複数ソースを 1 つの UiState に合成する

MutableStateFlow を何本も手で更新する代わりに、「ソースの合成結果 = UiState」と宣言できます。


combine(repo.items, query, filter) { items, q, f ->
    UiState.Success(items.filter { it.matches(q, f) })
}

 

🧑🏻‍💻 3. map: ドメインモデルから UI モデルへの変換

整形や表示用の変換はここに集約します。


repo.user.map { it.toUiModel() }

 

🧑🏻‍💻 4. flatMapLatest: 入力の変化で上流を切り替える

検索クエリや選択 ID が変わると、前のリクエストを自動キャンセルして新しい Flow に切り替えます。


query.flatMapLatest { q -> repo.search(q) }

 

🧑🏻‍💻 5. catch: エラーを UiState に変換する

例外を UI の状態として扱えます。上流のエラーだけを捕捉する点が try/catch との違いです。


.catch { emit(UiState.Error(it.message)) }

 

🧑🏻‍💻 6. onStart: Loading の初期値を流す

購読開始時に Loading を流す定番の書き方です。


repo.items
    .map<_, UiState> { UiState.Success(it) }
    .onStart { emit(UiState.Loading) }

 

🧑🏻‍💻 7. distinctUntilChanged: 無駄な再描画を防ぐ

StateFlow 自体も同値を弾きますが、中間の Flow で重い処理の前に挟むと効果的です。


query.distinctUntilChanged()
    .flatMapLatest { repo.search(it) }

 

🧑🏻‍💻 8. debounce: 入力の連打を間引く

検索ボックスなどで使います。debounce は同値の弾き込みはしないため、distinctUntilChanged と併用します。


query.debounce(300)
    .distinctUntilChanged()

 

🧑🏻‍💻 9. scan / runningFold: 状態を畳み込む

ページング風の追加読み込みや、イベントから状態を積み上げる MVI 的な設計で使います。


events.scan(UiState()) { state, event -> reduce(state, event) }

 

🧑🏻‍💻 10. flowOn: 上流の実行スレッドを指定する

重い変換や IO を Main から外します。上流にだけ効く点に注意してください。


repo.items
    .map { heavyTransform(it) }
    .flowOn(Dispatchers.Default)

 

🧑🏻‍💻 よくある組み合わせ例


class SearchViewModel(repo: SearchRepository) : ViewModel() {
    private val query = MutableStateFlow("")
    private val filter = MutableStateFlow(Filter.All)

    val uiState: StateFlow<UiState> = combine(
        query.debounce(300).distinctUntilChanged()
            .flatMapLatest { repo.search(it) },
        filter
    ) { results, f ->
        UiState.Success(results.filter(f::accepts)) as UiState
    }
        .onStart { 
            emit(UiState.Loading) 
        }
        .catch { 
            emit(UiState.Error(it.message)) 
        }
        .flowOn(Dispatchers.Default)
        .stateIn(
            viewModelScope, 
            SharingStarted.WhileSubscribed(5_000), 
            UiState.Loading
        )

    fun onQueryChange(q: String) { 
        query.value = q 
    }
}

 

🧑🏻‍💻 参考


Related Categories :  Android・Developmemt・GitHub・JetpackCompose・KMP・Kotlin・Kotlin Multiplatform Mobile・Newbie・Recommended・Trending