この章では、同じ要件の画面を題材に、

  • Reducer パターンを 使わない場合
  • Reducer パターンを 使う場合

の 2 つの実装を比較します。

目的は、 Reducer が「必要になる瞬間」がどこにあるのかを体感すること です。


想定する画面要件(共通)

  • 初期表示時にデータを読み込む
  • 読み込み中はローディング表示
  • 成功したら一覧を表示
  • 失敗したらエラーメッセージを表示

よくある、シンプルだが 状態は 3 種類ある画面 です。


Reducer を使わない場合

UI State

data class UiState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val error: String? = null
)

ViewModel

class SampleViewModel(
    private val repository: ItemRepository
) : ViewModel() {

    private val _state = MutableStateFlow(UiState())
    val state: StateFlow<UiState> = _state

    fun load() {
        _state.update { it.copy(isLoading = true, error = null) }

        viewModelScope.launch {
            runCatching {
                repository.loadItems()
            }.onSuccess { items ->
                _state.update {
                    it.copy(
                        isLoading = false,
                        items = items
                    )
                }
            }.onFailure { e ->
                _state.update {
                    it.copy(
                        isLoading = false,
                        error = e.message
                    )
                }
            }
        }
    }
}

特徴

  • コード量が少ない
  • 処理の流れが直線的で追いやすい
  • 小規模な画面では十分に読みやすい

一方で、

  • 状態更新ロジックが ViewModel に散らばる
  • 状態が増えると if / copy が増殖しやすい

という性質を持ちます。


Reducer を使う場合

UI State(同じ)

data class UiState(
    val isLoading: Boolean = false,
    val items: List<Item> = emptyList(),
    val error: String? = null
)

Event

sealed interface UiEvent {
    object Load : UiEvent
    data class LoadSuccess(val items: List<Item>) : UiEvent
    data class LoadError(val message: String) : UiEvent
}

Reducer(状態遷移の定義)

fun reduce(state: UiState, event: UiEvent): UiState =
    when (event) {
        UiEvent.Load ->
            state.copy(isLoading = true, error = null)

        is UiEvent.LoadSuccess ->
            state.copy(
                isLoading = false,
                items = event.items
            )

        is UiEvent.LoadError ->
            state.copy(
                isLoading = false,
                error = event.message
            )
    }

Reducer は、小規模なものであれば、 ViewModel 内に定義しても問題ありません。

ただし、規模が大きくなり始めたら、 ViewModel と同じディレクトリ内の別ファイルに定義すると良い場合が多いです。


ViewModel(StateHolder)

class SampleViewModel(
    private val repository: ItemRepository
) : ViewModel() {

    private val _state = MutableStateFlow(UiState())
    val state: StateFlow<UiState> = _state

    fun dispatch(event: UiEvent) {
        _state.update { reduce(it, event) }
    }

    fun load() {
        dispatch(UiEvent.Load)

        viewModelScope.launch {
            runCatching {
                repository.loadItems()
            }.onSuccess { items ->
                dispatch(UiEvent.LoadSuccess(items))
            }.onFailure { e ->
                dispatch(UiEvent.LoadError(e.message ?: "error"))
            }
        }
    }
}

特徴

  • 状態遷移のルールが Reducer に集約される
  • ViewModel から「状態変更の詳細」が消える
  • 画面の状態遷移がコードとして可視化される

一方で、

  • Event / Reducer の定義が増える
  • 単純な画面では冗長に感じやすい

というコストがあります。


どちらを選ぶべきか

判断基準はシンプルです。

Reducer を使わない方がよい場合

  • 状態が少ない
  • 状態遷移が直線的
  • ViewModel が十分に読みやすい

Reducer を使う価値が出る場合

  • 状態の組み合わせが増えてきた
  • ローディング中・エラー中の挙動が複雑
  • ViewModel に状態更新ロジックが散り始めた

この章のまとめ

Reducer パターンは、

  • 設計を「正しくする」ためのものではありません
  • 複雑になり始めた状態管理を、破綻させないための道具 です

最初から導入する必要はありません。 「つらくなったら導入する」くらいが、Android ではちょうどよい のです。