Android アーキテクチャは Event-Driven UI から State-Driven UI へ移行している

Google の公式 Android ガイダンスは 2026年5月18日に更新され、ViewModel のイベントを UI State に変換することを明示的に推奨するようになった

これは、もはや単なる昔からのアーキテクチャ論争ではありません。

Google の Android ドキュメントは 2026年5月18日 に更新され、現在では明確に次のように述べています。

ViewModel events should always result in a UI state update.

ViewModel のイベントは、常に UI State の更新につながるべきです。

これは、ViewModel と UI の間の通信をどのように考えるべきかという点で、大きな変化です。

これまで Android では、次のようなパターンが一般的でした。


ViewModel
   │
   ├── UiState
   │
   └── UiEvent
          ↓
         UI

UiEvent は、Snackbar の表示、画面遷移、ダイアログの表示など、一度だけ実行したい処理によく使われていました。

しかし現在のガイダンスは、次の方向へ移りつつあります


ViewModel
   │
   │ UI State
   ↓
  UI

ここでは、簡単な Snackbar の例を使って、この違いを見てみましょう。

 

🤔 Event-Driven アプローチ

従来の実装は、例えば次のようになります。


sealed interface UiEvent {
    data class ShowSnackbar(
        val message: String
    ) : UiEvent
}

private val _events = Channel<UiEvent>()
val events = _events.receiveAsFlow()

fun save() {
    viewModelScope.launch {
        repository.save()

        _events.send(
            UiEvent.ShowSnackbar("Saved successfully")
        )
    }
}

UI 側では、このイベントを消費します。


LaunchedEffect(Unit) {
    viewModel.events.collect { event ->
        when (event) {
            is UiEvent.ShowSnackbar ->
                snackbarHostState.showSnackbar(event.message)
        }
    }
}

この場合、ViewModel は実質的に UI に対して次のように指示しています。


"Snackbar を表示してください。"

これが Event-Driven なアプローチです。

 

🤔 State-Driven アプローチ

現在の Android ガイダンスは、上記とは異なる方向を示しています。

ShowSnackbar を送るのではなく、ViewModel が UI State を更新します。


data class UiState(
    val userMessage: String? = null
)


private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
        
fun save() {
    viewModelScope.launch {
        repository.save()

        _uiState.update {
            it.copy(userMessage = "Saved successfully")
        }
    }
}

fun userMessageShown() {
    _uiState.update {
        it.copy(userMessage = null)
    }
}

UI は State を監視します。


LaunchedEffect(uiState.userMessage) {
    uiState.userMessage?.let { message ->
        snackbarHostState.showSnackbar(message)
        viewModel.userMessageShown()
    }
}

この場合、ViewModel は次のように言っているわけではありません。


"Snackbar を表示してください。"

代わりに、次のように伝えています。


"現在、表示すべきメッセージがあります。"

どのように表示するのかは UI が決定します。

 

🤔 重要な違い

この違いは、単純に


Channel → StateFlow

という API の置き換えではありません。

本質的には次の違いです。

Event-Driven:


ViewModel
    ↓
"これを実行して"
    ↓
UI

State-Driven:


ViewModel
    ↓
"これが現在の状態です"
    ↓
UI

Google は、この違いを次のようにも表現しています。

「State は存在する。Events は発生する。」

もちろん、イベントそのものがなくなったわけではありません。

ボタンのクリックは、今でもイベントです。


User
 ↓
Button.onClick
 ↓
ViewModel
 ↓
State
 ↓
UI

重要な変化は、ViewModel → UI の通信が、一度きりのイベントではなく State を中心とする方向へ進んでいることです。

 

🤔 では、Event を使うのをやめるべきなのか?

公式ガイダンスは、Channel、SharedFlow、あるいはイベントそのものを禁止しているわけではありません。

むしろ、ViewModel のイベントを UI State として表現できないかを改めて考えることを求めています。

ViewModel を設計するときには、次の問いが役立ちます。

- ViewModel は UI に「何をすべきか」を伝える必要があるのか?

- 「現在何が起きているのか」を State として表現するだけでよいのか?

この小さな考え方の変化が、Android の UI アーキテクチャの設計を大きく変える可能性があります。

 

🤔 参考


Jake Wharton の State や UI アーキテクチャに関する名言

 

1.「UI は State の関数である」

"The view should be a function of state."

(View / UI というものは、状態の関数であるべきだ。)

UI に対して「表示せよ」「消せ」と個別命令(コマンド)を出すのではなく、ただ「現在の状態(State)」を渡して描画させるべきだという、現代の宣言的UI(Composeなど)の根本思想。

 

2.「イベントは過去の事実、コマンドは未来への指示」

"Events are things that happened in the past. Commands are instructions for the future. Don’t model events as commands."

(イベントとは『過去に起起きた事実』である。コマンドとは『未来への指示』である。イベントをコマンドとしてモデリングしてはならない。)

画面回転時の二重発火バグなどを防ぐため、UI から上がる「起きた事実(Event)」とロジックからの「操作指示(Command)」を明確に区別せよという警告。

 

3.「イベントは上がり、ステートは降りてくる」

"Events go up, State goes down."

(イベントは上へ上がり、ステートは下へ降りてくる。)

ユーザー操作(Event)は上層のロジックへ送信され、処理結果として更新された「State」だけが下層の UI へ降りてくる単方向データフロー(UDF)の本質を直感的に表したフレーズ。

 

4.「View はバカ(受動的)でいい」

"Views are dumb. They should just render whatever state is given to them, and report user events back up."

(View はバカ(受動的)でいい。渡された State をそのまま描画し、発生したユーザーイベントを上層に報告するだけの存在であるべきだ。)

UI 自身に複雑な if/else や描画切り替えロジックを持たせず、渡された State を忠実に描画するだけの存在に削ぎ落とすべしという指針。

 

5.「State は操作の差分ではなく、純粋なデータから作れ」

"State should be represented as data, not as a delta of commands."

(状態はコマンドの差分(delta)として表現するのではなく、純粋なデータとして表現すべきだ。)

「UI要素を少しずつ操作する差分積み上げ」ではなく、「画面全体のデータ構造(Data Class)」を一括で差し替えるべきだという命令型UIへの批判。

 

6.「Compose は UI ツールキットではなく、State の管理エンジンだ」

"Jetpack Compose is not a UI toolkit. It's an unbundled compiler plugin and runtime for managing a tree of nodes from state."

(Jetpack Compose は UI トゥールキットではない。State(状態)からノードのツリーを管理するための、独立したコンパイラプラグイン兼ランタイムだ。)

Compose の本質は「State の変化に連動してツリー構造を自動更新する汎用エンジン」であり、UI 以外のロジック層(MoleculeやCircuit)にも応用できるという洞察。

 

7.「Molecule: Compose ランタイムで StateFlow を生み出す」

"Molecule uses Jetpack Compose under the hood to convert any State / Action loop into a single StateFlow."

(Molecule は裏側で Jetpack Compose を使い、あらゆる State / Action のループを単一の StateFlow へと変換する。)

ViewModel 内で複雑な Flow をこねくり回す代わりに、Compose の宣言的構文(remember や LaunchedEffect)を使ってロジック層の State を生成するという新たなアプローチ。

 

8.「State が Mutable なら、そこにあるのはカオスだ」

"If your state is mutable, you don't have a single source of truth. You have chaos."

(もし君の状態(State)が可変(Mutable)なら、そこにあるのは『唯一の正しい情報源(Single Source of Truth)』ではなく、ただの『カオス』だ。)

UI 側が State を直接書き換える危険性を排除し、State は必ず Immutable(不変)に保ち、更新はインスタンスの再生成で行うべきだという強調。

 

9.「View を描画せずに UI ロジックをテストすることこそが聖杯だ」

"Testing UI logic without rendering views is the holy grail of modern UI architecture."

(View を描画することなく UI ロジックをテストすることこそが、現代の UI アーキテクチャの聖杯(悲願)だ。)

Circuit などの設計理念。重い Android 端末やエミュレータを起動せず、純粋な JVM の高速な単体テストで UI ロジックを完結させる重要性。

 

10.「状態の再現(Recreation)と永続化(Persistence)を混同するな」

"State restoration after process death is not the same as data persistence. One is temporary UI continuity, the other is long-term storage."

(プロセス死後の状態復元は、データの永続化とは異なる。前者は一時的な UI の継続性であり、後者は長期的なストレージ保存だ。)

SavedStateHandle(あるいは旧 onSaveInstanceState)に保存すべき「画面の途中経過(スクロール位置や未送信の入力文言)」と、データベースや DataStore に保存すべき「アプリの永続データ」を明確に分けよという設計指針。

 

11. 「副作用(Side Effect)を状態(State)の中に紛れ込ませるな」

"Side effects should be explicit responses to state changes, not hidden steps inside state mutations."

(副作用は状態の変化に対する明示的な反応であるべきであり、状態の変更処理の中に隠されたステップであってはならない。)

データベースの書き込みやネットワークリクエストといった「副作用」を、State の更新ロジック(Reducer や Setter)の中にコッソリ埋め込むなという注意喚起。Side Effect は State の変化(あるいは操作イベント)を受けて独立して発火させるべきだと述べました。

 

12.「状態を暗黙的に共有するな。明示的に流せ」

"Implicit shared state is the root of all evil in UI development. Pass state explicitly down the tree."

(暗黙的に共有された状態は、UI 開発におけるあらゆる悪の根源だ。状態はツリーを伝って明示的に下に渡せ。)

グローバルなシングルトンや静的変数に状態を持たせてあちこちから参照・書き換えをする設計を強力に否定。現在で言えば、Compose の LocalContext や CompositionLocal にビジネス状態を安易に突っ込まず、パラメータ(Props)として明示的に State を渡す「State Hoisting」の重要性に通じます。

 

13.「状態の変更経路(Mutation Path)は常に1つに絞れ」

"There should only ever be one pathway through which a piece of state can change."

(ある1つの状態が変化する経路は、常に1つだけでなければならない。)

いわゆる Single Source of Truth(唯一の正しい情報源) と Unidirectional Data Flow(単方向データフロー) の根幹。複数の場所から状態を書き換えられるようにすると、どのイベントが原因で UI が壊れたのか追跡不能(カオス)になるという指摘です。

 

14.「UI テストが壊れやすいのは、View に状態を持たせているからだ」

"Fragile UI tests are a symptom of UI components holding and managing their own implicit state."
(壊れやすい UI テストは、UI コンポーネント自体が暗黙の状態を保持・管理してしまっていることの徴候だ。)

Espresso や UI テストが少しの変更で落ちやすくなる原因は、UI(View/Composable)自体に「状態の変化ロジック」が残っているからであり、UI を純粋な「State を受け取って表示するだけの関数」にできれば、ロジックのテストは超高速な Unit Test だけで完結すると説きました。

 

15.「非同期処理のキャンセル(Cancellation)も State の一部だ」

"Cancellation is not an error; it's a first-class state transition."

(キャンセル処理はエラーではない。ファーストクラスの『状態遷移』である。)

画面遷移やスクロールによって不要になった通信や処理をキャンセルすることは、例外処理(Exception)として捉えるのではなく、「処理中」から「破棄/無効」という正常な状態の遷移として設計・ハンドリングすべきだという主張。

 

16.「状態の集約(Aggregation)は、UI に届く前に終わらせろ」

"Do not make the UI assemble state from multiple streams. Combine them in the logic layer and expose one unified State."

(UI に複数のストリームから状態を組み立てさせるな。ロジック層でそれらを結合(Combine)し、単一の統一された State として公開せよ。)

Composable や View の中で streamA と streamB をそれぞれ Observe して if (a && b) のようなロジックを書くのを禁止する教義。combine(flowA, flowB) { a, b -> UiState(...) } のように、ViewModel/Presenter 側で1つの UiState にまとめ上げてから UI に渡すべしという指針。

 

17.「StateFlow は最新の値(State)を持ち、SharedFlow は出来事(Event)を流す」

"StateFlow represents a value over time that can be read at any moment. SharedFlow is for broadcasting events where history may not matter."

(StateFlow は常に読み取れる『時間経過に伴う値』を表す。SharedFlow は履歴が重要ではない『出来事のブロードキャスト』のためにある。)

Coroutines Flow 導入期に、開発者が StateFlow と SharedFlow の使い分けに混乱していた時期の発言。状態(State)は常に「現在の最新値」を保持・復元できる必要があり、イベント(Event)は「通過するストリーム」であるという本質的な違いを明確化しました。

 

18.「可変性(Mutability)は関数の中に閉じ込めろ」

"Local mutability is completely fine. Shared mutability is poison."

(関数ローカルな可変性は何の問題もない。共有された可変性こそが毒なのだ。)

「一切の var や可変オブジェクトを排除せよ」という極端な関数型プログラミング信者に対する現実的なアドバイス。関数内部でループや計算のために var や MutableList を使うのは何の問題もないが、それが関数の外にリーク(共有)した瞬間にバグの温床(カオス)になるという指摘です。

 

19.「状態の表現(Types)にドメインの制約(Invariants)を埋め込め」

"Make invalid states unrepresentable in your type system."

(型システムによって『不正な状態』を表現不可能にせよ。)

例えば data class UiState(val isLoading: Boolean, val data: Data?, val error: Throwable?) だと「ロード中なのにデータもエラーもある」という不正な状態(Invalid State)が作れてしまう。これを sealed class や sealed interface を使って Loading, Success(data), Error(err) に分離することで、物理的に「不正な状態になり得ない型定義」にせよという教義。

 

20.「アーキテクチャとは『どこに状態(State)を置くか』の決定にすぎない」

"Architecture is fundamentally just deciding where state lives and how it moves."

(アーキテクチャとは、根本的には『状態がどこに生存し、それがどう移動するか』を決めることにすぎない。)

設計パターン(MVVM, MVI, Clean Architecture など)の複雑な層構造や命名に惑わされている開発者に向けた本質的な一言。結局のところ、状態がどこで作られ、どこに保存され、どう流れるか(UDF)を整理することこそがアーキテクチャの真髄であるとまとめました。

 

21.「暗黙の副作用(Implicit Side-effects)はデバッグの敵だ」

"If an operation changes state, it must be explicit. Hidden state changes are the hardest bugs to trace."

(操作が状態を変更するなら、それは明示的でなければならない。隠された状態変更こそが、最も追跡困難なバグだ。)

プロパティの getter や setter の内部、あるいは「単なる読み取り用の関数」に見える場所で、裏でこっそり内部フラグや状態を書き換えるコードを厳しく戒めた発言。状態の変化(State Mutation)は必ず明示的な関数名やイベントハンドラを通じて行われるべきだと主張しました。

 

22.「UI レイヤーに 『時間の概念』を持ち込むな」

"The UI should not care about time. Debouncing, throttling, and delay belong to the state pipeline, not the View."

(UI は 『時間』の概念を気にすべきではない。デバウンス(Debounce)やスロットリング(Throttle)、ディレイ処理は View ではなく State のパイプラインに属するものだ。)

連打防止のための Handler.postDelayed や Compose 内での delay() を使ったボタン制御などを否定。検索窓のインクリメンタルサーチやボタンの連打制御など、「時間」が絡むロジックはすべて ViewModel / Presenter 側の Flow パイプライン(debounce(), sample() など)で処理し、UI には「今表示すべき State」だけを渡すべきだという教義です。

 

23.「状態の生存期間(Scope)を UI の生存期間と一致させるな」

"Do not tie the scope of your state to the lifespan of a single UI component, unless that state dies with it."

(その状態が UI と一緒に死ぬものでない限り、状態のスコープを単一 UI コンポーネントの生存期間に縛り付けるな。)

Fragment や Composable の「画面破棄」と「画面背後で進行している処理(ダウンロードや送信処理)」のスコープを切り離す設計の重要性。UI が回転やスタック遷移で消えても、状態を管理するスコープ(ViewModelScope や CoroutineScope)は独立して生存し、再び UI が復元された際に最新の State を再注入(Re-attach)できる構造にせよと説きました。

 

24.「宣言的 UI の真価は Recomposition(再構築)の予測可能性にある」

"Declarative UI is not just less code; it’s making UI updates predictable by reducing state space."

(宣言的 UI の本質は単にコード量が減ることではない。状態空間(State Space)を削減することで、UI の更新を予測可能にすることだ。)

Compose などの宣言的 UI が優れている最大の理由は「コードの短さ」ではなく、if/else や手動の UI 更新命令で発生しがちだった「あり得ない画面状態
(不整合な組み合わせ)」を排除し、State から UI へのマッピングを 1:1 の決定論的(Deterministic)な関数にすることだと述べました。

 

25.「単方向(Unidirectional)とは、逆流を許さないということだ」

"Unidirectional data flow means data flows one way. If your view is updating the model directly, your flow is bidirectional and broken."

(単方向データフローとは、データが一方向にしか流れないことを意味する。もし View が Model を直接更新しているなら、それは双方向(Bidirectional)であり、崩壊している。)

2-way Data Binding(双方向データバインディング)のように、UI 側のテキスト入力が自動的に Model のプロパティを直接書き換える仕組みへの強烈な牽制。UI はあくまで「テキストが変更された」という Event を発行し、それを受けたロジック層が新しい State を生成して下に降ろすという「一方向の円環」を崩してはならないと emphasized しました。

 

26.「表示の遅延(Latency)も State としてデザインせよ」

"Loading is not a lack of data; it is a discrete, valid state of your UI."

(ローディング(読み込み中)は『データが無い状態』ではない。それ自体が独立した、正しい UI の『状態』である。)

データを取得するまでの待ち時間や遅延を「データの不在(null)」として扱うのではなく、明確に設計された State.Loading という状態としてモデリングすべきだという主張。data == null でローディングを判定するようなズボラなコードが、Skeleton View やプログレス表示のチラつき(Flicker)バグを生む原因になると戒めました。

 

27.「状態の同期(Synchronization)に頼るな。計算(Computation)で導出せよ」

"Don't synchronize two states if you can derive one from the other."

(一方から導出できるのであれば、2つの状態を『手動で同期』させようとするな。)

例えば「リストの全要素(items)」と「検索結果の要素数(count)」という2つの変数を持たせて手動で count を更新するようなコードへの批判。count は items.size から派生して計算(Derive)されるべきものであり、複数の状態を手動で同期させようとした瞬間に必ず同期漏れのバグが発生すると指摘しました。

 

28.「Redwood: 状態のツリーをマルチプラットフォームで共有する」

"Redwood applies the same Compose state-to-tree paradigm to arbitrary UI toolkits across Android, iOS, and Desktop."

(Redwood は、Compose の『State からツリーを生成するパラダイム』を、Android, iOS, Desktop を跨ぐ任意の UI トゥールキットに適用するものだ。)

Jake が開発をリードした Cash App Redwood(のちに Circuit の下地にもなったライブラリ)の思想。State から作られる UI ツリーの構築ロジックを KMP(Kotlin Multiplatform)としてロジック層に完全に切り離せば、描画エンジンが Android Native(View/Compose)であれ iOS(SwiftUI)であれ、完全に同じ State / Logic を共有できるという宣言的UIの究極系を示しました。

 

29.「エラー状態は特殊な分岐ではなく、State の変奏曲(Variant)にすぎない」

"Errors in UI are not control flow exceptions; they are just data waiting to be rendered."

(UI におけるエラーは制御フローの例外(Exception)ではない。ただ描画されるのを待っている『データ』にすぎない。)

API 通信などの例外が発生した際、try-catch で UI の表示を強引に切り替える(制御フローとして扱う)のを禁止する考え方。例外が発生した場合も、それを捕捉して State.Error(message) という純粋な「データ(State)」に変換して UI に渡すべきであり、UI 側はエラーすらも「描画すべきデータの種類のひとつ」としてフラットに処理すべきだと説きました。

 

30.「アーキテクチャの良し悪しは、状態(State)を巻き戻せるか(Time Travel)で測れる」

"A clean architecture allows you to replay a stream of events and recreate the exact UI state at any point in time."

(優れたアーキテクチャとは、イベントのストリームを再生(Replay)することで、過去の任意の時点における UI 状態を正確に再構築できるものである。)

MVI / UDF の最大の強みであるデバッグ可能性(Time Travel Debugging)についての発言。すべての状態が「初期 State + イベントの累積」で計算可能になっていれば、起きたイベントのログを順番に叩き込むだけで、ユーザーの手元で起きたバグの画面状態を開発環境で 100% 完全再現できると説明しました。

 

31.「State の比較(Equals)は安く、UI の再描画(Recomposition)は重い」

"State comparison is cheap; rendering UI is expensive. Always design your states so that distinct structural equality skips unnecessary redrawing."

(State 同士の比較(equals)は軽量だが、UI の描画はコストが高い。構造的等価性(data class の == 比較)によって不要な再描画をスキップできるよう、常に State を設計せよ。)

Compose などの宣言的 UI において、State が data class や @Stable / @Immutable に適切に定義されていれば、フレームワークは「State が変わっていない」と判断して描画スキップ(Skipping)を完璧に行えるというパフォーマンス原則。State の定義をテボラに作ると、画面全体の無駄な Recomposition を引き起こす原因になると警告しました。

 

32.「データベースは、State の『永続化されたプロキシ』に過ぎない」

"A database table is not a entity separate from your UI; it's just a persisted, observable stream of state."

(データベースのテーブルは UI と分離された別物ではない。ただの『永続化され、観察可能な State のストリーム』に過ぎない。)

彼が手がけた SQLDelight の設計思想。DB は「クエリを発行してデータを抜き取る場所」ではなく、「DB の変更を Flow / Observable として監視(Observe)し、そのまま State パイプラインに流し込む元泉(Single Source of Truth)」として扱うべきだと主張しました。

 

33.「状態(State)をモジュール境界(Module Boundaries)にするな」

"Expose behavior and events across feature modules, not raw mutable state structures."

(機能モジュール間(Feature Modules)で公開すべきなのは『振る舞い(Behavior)とイベント(Event)』であり、生の可変状態構造(Mutable State)ではない。)

マルチモジュール構成における警告。ある Feature モジュールが別の Feature モジュールの内部 State や ViewModel に直接依存してしまうと、モジュール間の結合度が跳ね上がり、ビルド時間の悪化や状態の衝突を引き起こす。モジュール境界を跨ぐときは純粋な Interface や Event / Intent のやり取りに留めるべきだと説きました。

 

34.「画面(Screen)単位で ViewModel を刻むな。機能(Feature)の境界で刻め」

"ViewModels shouldn't blindly map 1:1 to every single XML or Composable file. They map to boundaries of state ownership."

(ViewModel をすべての XML や Composable ファイルに機械的に 1:1 で対応させるな。ViewModel は『状態の所有権(State Ownership)の境界』に対応させるものだ。)

「1画面=1 ViewModel」という固定観念に対する指摘。小さなコンポーネントや単なるリレイアウト部分にまで ViewModel を作成すると管理が煩雑になる。ViewModel / State Holder は、明確に自律したビジネスロジックや状態の持ち主(State Owner)ごとに割り当てるべきだと説明しました。

 

35.「状態の「型(Type)」をケチるな。明確な名前(Explicit Naming)を与えよ」

"Don't reuse generic tuples or Pair/Triple to represent UI state. Give every state a clear, domain-specific name."

(UI State を表現するために、汎用的な Pair や Triple、Tuple を使い回すな。すべての State に明確でドメイン固有の名前を与えよ。)

Pair<Boolean, List<Item>> のような手を抜いた型定義を厳禁とする教義。型自体がドメインの文脈 SearchUiState(val isSearching: Boolean, val results: List<Item>) を自己説明していなければ、コードの可読性が失われ、State パイプラインの追跡が困難になると強調しました。

 

36.「状態(State)はシリアライズ(直列化)できなければ本物ではない」

"If your state cannot be serialized, it’s not truly state; it’s just transient in-memory behavior."

(もし君の状態(State)がシリアライズできないのなら、それは真の状態ではない。ただの過渡的なメモリ上の振る舞いにすぎない。)

プロセス死(Process Death)や画面遷移のスタック管理において、UI State や Navigation State は kotlinx.serialization や Parcelable などで復元可能なデータ構造(Value Class / Data Class)として定義されるべきだという主張。ラムダ式やコンテキスト(Context)といった「復元不可能なオブジェクト」を State に混ぜ込むなという警告です。

 

37.「非同期ストリームの『完了(Completion)』と『状態(State)』を混同するな」

"A StateFlow never completes. If your flow completes, it’s representing a task, not a state."

(StateFlow は決して完了しない。もし君の Flow が完了するなら、それは『状態』ではなく『タスク(処理)』を表している。)

Coroutines における根本的な概念の分離。データ通信や計算タスク(Task)は終了(Complete/Success/Error)を迎えますが、画面やコンポーネントが生きている限り、それを監視する StateFlow は常に「現在の値」を提示し続ける無限のストリームであるべきだと指摘しました。

 

38.「State をグローバルに叫ぶな。局所化(Locality)せよ」

"Scope your state to the exact boundary of who needs it. Globalizing state is just lazy engineering."

(状態はそれを必要とする最小限の境界の中にスコープせよ。状態をグローバル化するのは単なる怠惰なエンジニアリングだ。)

アプリ全体の共通領域(Singleton や GlobalScope)に安易に State を置く設計への批判。「画面全体の State」「コンポーネントごとの State」「ダイアログの State」など、各ツリーのノードが最小限のスコープで責任を持って状態を保持すべきだと述べました。

 

39.「テストコードのセットアップが長すぎるのは、State が肥大化(Fat)している悲鳴だ」

"If your test setup requires mocking ten dependencies to verify a state change, your state holder knows too much."

(状態の変化を検証するだけで10個もの依存をモックしなければならないなら、君の State Holder(ViewModel/Presenter)は知りすぎている。)

テストの書きやすさを設計の良悪のバロメーターとする教義。Presenter や ViewModel が大きくなりすぎている(Fat)場合、小さな State / Event のサブユニットに分割するか、Use Case や Reducer として切り出すべきだというアドバイスです。

 

40.「状態(State)は『過去の履歴』を持つな。『現在の事実』だけを持て」

"State represents the present. Do not pollute state with event history unless the history itself is part of the domain requirement."

(State は『現在』を表すものだ。ドキュメントの履歴表示などドメイン要件自体が歴史を求めている場合を除き、過去のイベント履歴で State を汚染するな。)

「過去にどのボタンが押されたか」「どのエラーが通過したか」といった履歴情報を State のプロパティとして引きずり回すアンチパターンへの指摘。State は常に「たった今、UI がどう描画されるべきか」という現在値のみを保持すべきであると説きました。

 

41.「State の型に非ドメインな構造(UI フレームワーク要素)を混ぜるな」

"Never leak framework-specific types into your State. Your State should be pure Kotlin, completely detached from Android or iOS."

(State の中にフレームワーク固有の型をリークさせるな。State は完全に Android や iOS から独立した、純粋な Kotlin であるべきだ。)

UiState のプロパティに Bitmap, Drawable, Color, Context といった Android API 固有のオブジェクトを持たせるなという徹底した教義。これらを混ぜると、JVM 上での超高速な単体テストが不可能になり、Kotlin Multiplatform (KMP) への移植性も失われます。

 

42.「初期状態(Initial State)の定義を曖昧にするな」

"Every State pipeline must have a deterministic, synchronous initial value before any async work begins."

(あらゆる State パイプラインは、非同期処理が始まる前に、決定論的かつ同期的に読み取れる『初期値』を持たなければならない。)

StateFlow が initialValue を強制する理由であり、remember や collectAsState が初期値を求める理由。非同期通信が完了するまでの「コンパイル可能で破綻しない初期 State(例: Loading や空の状態)」を定義できない設計は、画面初期化時のヌルポやチラつき(Flicker)の原罪になると指摘しました。

 

43.「状態の階層構造(State Nesting)は浅く保て」

"Prefer flat State classes over deeply nested structures. Flat state makes copying and recomposition tracking dramatically simpler."

(深くネストされた構造よりも、フラットな State クラスを好め。フラットな State は copy() や Recomposition の追跡を圧倒的にシンプルにする。)

state.user.profile.address.city のように State の中にオブジェクトを深層ネストさせると、一部分を更新する際に copy(user = user.copy(profile = ...)) とコードが惨状と化し、Recomposition の不要な発火を引き起こしやすい。状態の持ち方は可能な限りフラットなデータ構造にまとめよと教え示しました。

 

44.「状態の変化(State Change)と、表示のアニメーション(Animation)を混ぜるな」

"Animation is a rendering concern. State dictates 'what is shown', while the UI layer decides 'how it transitions'."

(アニメーションは描画の関心事だ。State は『何が表示されているか』を命じ、UI レイヤーが『どう遷移(アニメーション)するか』を決定する。)

State の中に animatingIn: Boolean や fadeInProgress: Float といったアニメーションの途中経過プロパティを持たせる設計への否定。State はあくまで「現在の論理的な表示状態(例: Visible / Hidden)」を保持し、それをどう画面上で動かすか(AnimatedVisibility など)は UI 側のレンダリングロジックに委ねるべきだと主張しました。

 

45.「遷移(Navigation)とは、本質的に『Navigation State』の変更にすぎない」

"Navigation is not about switching screens; it's about mutating a backstack state value in a predictable stack."

(ナビゲーションとは『画面を切り替えること』ではない。予測可能なスタックにおける『バックスタック状態(State)の値の書き換え』である。)

画面遷移を「Activity や Fragment のインテント発火(Command)」と捉えるのではなく、「画面の履歴スタック(List)という1つの State を更新する操作」と定義した視点。現在話題の Navigation 3.0 や Circuit の Navigator が、画面スタックをただの List というシリアライズ可能な State として扱う根幹の思想です。

 

46.「副作用(Effect)は消費されるべきものであり、観察(Observe)されるべきものではない」

"Events and side-effects are meant to be consumed once, while state is meant to be observed continuously."

(イベントや副作用は『1回消費されるべきもの』であり、ステートのように『継続的に観察され続けるもの』ではない。)

StateFlow(継続観察)と Channel / SharedFlow(1回消費)の決定的な役割の違いを突いた名言。一回限りのアクション(Toast や Analytics 送信など)を継続観察可能な State に混ぜ込むと、再描画(Recomposition)や画面回転のたびに副作用が意図せず再発火する根本原因になると説きました。

 

47.「ドメインモデルを直接 UI State に渡すな。表示用の防波堤を作れ」

"Do not pass your raw domain or network models straight into the UI. Map them to a tailored UI State."

(生(Raw)のドメインモデルやネットワークモデルをそのまま UI に渡すな。UI 専用に調整された UI State にマッピングせよ。)

API のレスポンス型や DB の Entity 型をそのまま Composable や View に引数として流し込むズボラな設計に対する警告。ドメインモデルの仕様変更(フィールド名の変更や型の追加)が直接 UI 描画の崩壊に直結するのを防ぐため、必ず UI 層の手前で UiState へマッピングする「層の分離(Decoupling)」を徹底すべしと求めました。

 

48.「UI の『状態の崩壊』は、暗黙的なデフォルト値(Implicit Default)から始まる」

"Avoid implicit defaults in your state models. Explicit initial conditions prevent sneaky UI race conditions."

(State モデル内で暗黙的なデフォルト値("" や 0 や false)を多用するな。明示的な初期状態の定義こそが、忍び寄る UI のレースコンディション(競合)を防ぐ。)

data class UiState(val isError: Boolean = false, val data: String = "") のように安易なデフォルト値を持たせると、「まだ初期化されていない状態」と「正常に空データが取得できた状態」の区別がつかなくなり、初期化時のチラつきやレースコンディションを招く。初期状態は Loading などの型で明示的に区別すべきだと強調しました。

 

49.「State Holder に UI フレームワークのコンテキスト(Context)を渡すな」

"If your ViewModel or Presenter needs an Android Context, your state design has failed."

(もし君の ViewModel や Presenter が Android の Context を必要としているなら、状態の設計は失敗している。)

AndroidViewModel のように Context(や Resource)を直接ロジック層に持ち込むアプローチへの痛烈な批判。文字列のリソース ID(R.string.x)や文字列フォーマットの解決は、ViewModel 側で String 型のテキストデータ(またはリソースをカプセル化した Wrapper)に変換してから UI に渡すか、UI 描画時(Compose 側)に解決させるべきであり、ロジック層をフレームワーク依存から守るべきだと説きました。

 

50.「アーキテクチャの真価は、新しい機能を足す時ではなく、古い状態(State)を破棄する時にわかる」

"The test of a good architecture is not how easily you can add state, but how safely you can discard it."

(優れたアーキテクチャの真価は、どれだけ簡単に状態を追加できるかではなく、どれだけ安全に不要な状態を破棄(Discard / Clean up)できるかで試される。)

画面を離脱した際や機能を使わなくなった際、保持していたメモリ、非同期タスク、キャッシュなどの State が漏れ(Memory Leak)なく確実にクリアされる構造になっているか。状態の追加だけでなく「安全なクリーンアップ(onCleared / Coroutines のキャンセル)」まで見越した単方向設計こそが、プロダクトを息長く存続させると締めくくりました。

 

🤔 まとめ

1. UI = f(State) の徹底: UI は状態を受け取って描画するだけの単なる「関数」に過ぎない。

2. 命令(Command)から状態(State)への転換: UI のメソッドを叩いて操作指示を出す設計を全廃する。

3. データフローの単方向化(UDF): イベント(上り)とステート(下り)を明確に分け、逆流を許さない。

4. 型システムによる安全性: 不正な状態を型レベルで表現不能にし、シリアライズ可能・Immutable に保つ。

5. フレームワークからの解放: State や Logic を純粋な Kotlin コードに保つことで、超高速な Unit Test と KMP(Multiplatform)への展開を実現する。

この50個の教義を意識して Compose や ViewModel / Circuit を設計することで、バグがなくメンテナンス性に富んだ現代的な Android / Multiplatform アプリケーションを構築することができます。


Kotlin by JetBrains「Jake Wharton | KotlinConfersations'26」の文字起こし

Android開発やKotlinコミュニティのキーパーソンであるジェイク・ウォートン(Jake Wharton)氏へのインタビューです。

 

🤔 1. 自己紹介とKotlinとの歩み [00:26]

経歴:
長年Androidデベロッパーとして活動。Square(現Block)、Cash Appを経て、現在はSkylightに在籍。

Kotlinとの出会い:
Square時代、Java 7の機能不足やGoogleのツール進化の遅さに悩んでいた「プレ1.0(正式リリース前)」の頃にKotlinに注目。社内向けに導入提案書(プロポーザル)を作成。

反響:
その提案書を公開したところコミュニティで大きな話題となり、SquareでのKotlin導入だけでなく、エコシステム全体の盛り上がりに繋がった。

 

🤔 2. AndroidにおけるKotlinとコミュニティの力 [01:50]

Googleでの活動:
一時期Googleに籍を置き、AndroidにおけるKotlinの公式サポート(KTXライブラリなどの立ち上げ)を支援。

エコシステムの進化:
KTXライブラリのコード自体は現在通常のライブラリに統合されて役目を終えたが、それは言語が成熟した良い証拠であると語る。

Apple(Swift)とのアプローチの違い:
iOSのSwiftがAppleという中央集権からトップダウンで提供されたのに対し、AndroidにおけるKotlinは「草の根(グラスルーツ)的なコミュニティの熱意」から始まり、最終的にGoogleが公式サポートせざるを得ない流れを作った点がユニークである。

非Androidへの広がり:
「KotlinはGoogle製でも、Android専用でもない」という点が、15年経った今ではAndroid以外の開発者にも広く認知されるようになった。

 

🤔 3. Kotlin Multiplatform (KMP) の挑戦 [05:35]

これまでのクロスプラットフォーム:
過去のXamarinやPhoneGap、現代のReact NativeやFlutterなどを評価した上で、Cash App時代にKotlin Multiplatform(KMP)とCompose Multiplatformを選択。

アプローチの特徴:
UI層はiOSならUIKit/SwiftUI、WebならHTML DOM、AndroidならCompose UIといった「各プラットフォーム独自のネイティブビュー」を尊重し、バックエンドのビジネスロジックやプレゼンターロジックのみを共通化(シェア)する方針をとった。

KMPの強み:
他のクロスプラットフォーム言語と異なり、WebならJavaScript、iOSならネイティブコード、AndroidならJavaバイトコードと、ターゲットごとに最適な形へコンパイルされるため、メモリ空間の競合や相互運用の壁(Interop layer)が少ない。

エコシステムへの貢献:
5年前に始めた当時はJetBrainsのComposeも初期段階だったため、自分たちで足りないターゲットを作るなどしてエコシステムを強力にプッシュした。

 

🤔 4. オープンソース(OSS)の重要性 [10:53]

キャリアへの影響:
オープンソースに貢献することで、自身のスキルアップだけでなく、新しい人との出会いや転職の機会など、キャリアの節目で何度も救われた。

企業とOSSの関係:
REST APIとの通信や画像読み込み、依存関係の解決(DI)など、どのアプリでも共通する「ビジネスの知的財産(IP)ではない部分」のコードはオープンにすべき。結果的に会社の採用活動(「OSSを見て応募した」という優秀な人材の獲得)など、数字に表れにくい大きなリターン(無形の資産)をもたらす。

 

🤔 5. 生成AI(LLM)とライブラリの未来 [15:11]

「AIがコード生成できるならライブラリ投資は不要か?」という問いへの持論:
「コードを書くこと(Writing code)」自体は、ソフトウェア開発において決して最も難しい部分ではない。AIは既存のスキルを加速させる(アクセラレーター)ツールとして使うべきであり、それなしではコードが書けないような依存の仕方はリスクがある(将来的なコスト高騰も含めて)。

コードは書かれる回数より、読まれる回数の方が10倍多い(Code is read 10 times more than it's written)。 要件は常に変わるため、全体を俯瞰して理解できるコード設計が重要。

ライブラリの役割:
ライブラリの真の価値は「再利用可能なコードの境界線(デリミテーション)」を明確に引き、人間の認知負荷を下げることにある。パレートの法則のように「80〜85%の共通ユースケース」を綺麗にカプセル化することが大切。人間にとって直感的で優れた設計は、言語モデル(AI)にとっても扱いやすいはずである。

 

🤔 6. 2026年現在のKotlinへの期待 [22:45]

K2コンパイラの恩恵:
長年開発が続けられてきた「K2コンパイラ」への移行(大きな山場)を乗り越えたことで、言語自体の進化スピードが再び加速している。

注目している新機能:
Rich Errors(エラー処理の改善)プロポーザルの刷新
未使用の戻り値チェッカー(Unused return value checker)
when 式のデフォルト網羅性(Exhaustive when by default)

現在の心境:
K2の開発に全力を注いでいた停滞期を抜け出し、標準ライブラリ(datetimeやco-routinesなど)や言語プロポーザルが活発に進化している今の状況は、初期のKotlinのワクワク感を思い出させる。

 

🤔 7. コミュニティへの参加に気後れしている人へのアドバイス [27:30]

提案書(KEEP)の難しさ:
KEEP(Kotlin Evolution and Enhancement Process)はコンパイラの構文解析や各プラットフォームのバイトコードまで考慮しなければならず、内容が非常に高度で圧倒されるのは当然。

おすすめの関わり方:
Kotlin公式Slack(Kotlinlang Slack)にある language-proposal や language-evolution チャンネルを覗いてみるのがおすすめ。そこでは、より人間的で身近な困りごとや質問が、消化しやすい形で活発に議論されている。


Androidアーキテクチャの現状:GoogleのUDFと現実世界の「MVI風MVVM」の比較

現代のAndroid開発においてどのアーキテクチャを採用するかを議論する際、議論はほぼ常にMVVM対MVIのどちらにするかという点に集約されます。

しかし、近年の傾向を見ると、この2つは対立する力ではなく、収束しつつあることは明らかです。

 

🧑🏻‍💻 Googleの立場:特定のパターン名よりもUDFを優先

Googleの公式「アプリアーキテクチャガイド」を詳しく見てみると、「MVI」という用語はほとんど出てこないことに気づくでしょう。その代わりに、Googleは一貫してUDF(単方向データフロー)という概念を推奨しています。

Googleは、コミュニティに既に馴染みのある「MVI」ではなく、なぜ抽象的な用語である「UDF」を使用するのでしょうか?

その理由は、Googleが柔軟性を重視していることにあり、彼らの哲学は次のようなものと思われます。

「MVIのような厳格な命名規則にとらわれすぎないでください。アーキテクチャの本質である一方向のデータフローが維持されるようにするだけで十分です。」

Reduxのような厳密な状態管理ライブラリを使用する場合でも、シンプルなViewModelで実装する場合でも、Googleが重視するのは「状態は下へ流れ、イベントは上へ流れる」という基本原則に従うことです。

 

🧑🏻‍💻 現代の標準:MVI風MVVMの実装

現場では、Googleが推奨するUDFに対する最も一般的なアプローチは、私が「MVI風味のMVVM」と呼ぶもので、MVIルールをMVVMコンテナに詰め込むものです。これには、状態、意図、効果という3つのコア要素が含まれます。

実際には以下のようになります。


Hiltのナビゲーション引数で SavedStateHandle から卒業する

なぜ Hilt 2.49+ と @AssistedInject が、完全な型安全性を備えた引数渡しの「正解」と言えるのか。

 

🤔 問題点:静的な依存関係と動的なデータの混在

Android開発において、ViewModelにランタイム引数(実行時引数)を注入することは、常に議論の的となってきました。

その本質は、「静的依存関係」(DIによって管理されるリポジトリなど)と、「動的データ」(画面遷移時に渡されるナビゲーション引数など)の対立にあります。

適切なDIパターンを適用せずにこの問題に対処しようとすると、高確率でアンチパターンに陥ります。たとえば、一時的な状態をシングルトン(Singleton)内に保持してしまうと、状態の汚染や画面間でのデータ漏洩といった致命的なバグを引き起こす危険性があります。

 

🤔 解決策:ハイブリッド注入を実現する @AssistedInject

この「静的」と「動的」のギャップを埋めるための最適なツールが、@AssistedInject です。これを使用することで、DI(Hilt)が管理するオブジェクトと、実行時に受け取るランタイムパラメータをスマートに組み合わせた「ハイブリッド注入」が可能になります。

通常の @Inject は不要 :
@AssistedInject を使用する場合、コンストラクタに通常の @Inject を付ける必要はありません(というか、付けてはいけません)。

関心の分離 :
リポジトリなどの依存関係は Hilt が自動で供給し、ナビゲーション引数などの動的なデータだけを手動で安全に渡す、という明確な役割分担(関心の分離)が実現します。

クリーンなコードへ :
これにより、バグの温床になりがちだった危険な lateinit var による後からの初期化コードとは、完全におさらばできます。


// ViewModel 実装

@HiltViewModel(assistedFactory = RouteBViewModel.Factory::class) 
class  RouteBViewModel  @AssistedInject constructor( 
    private val repository: MyRepository,
    @Assisted val navKey: RouteB,
) : ViewModel() { 

    @AssistedFactory
    interface Factory { 
        fun create (navKey: RouteB) : RouteBViewModel 
    } 
}

 

🤔 実装:assistedFactory と hiltViewModel の組み合わせ

公式の nav3-recipes リポジトリでは、Hilt 2.49 以降で導入された最新の標準的な実装パターンが示されています。

ファクトリの宣言 :
@HiltViewModel(assistedFactory = ...) を使用して、Assisted Factory を ViewModel に直接リンクさせます。

2つの型パラメータ: Compose 側では、hiltViewModel() のように両方の型を明示的に指定して呼び出します。

安全なライフサイクル管理 :
creationCallback を利用することで、ナビゲーションのバックスタック管理(Lifecycle)を壊すことなく、安全にランタイム引数を渡すことができます。


// UI(ナビゲーション定義)

 entry<RouteB> { key -> 
    val viewModel = hiltViewModel<RouteBViewModel, RouteBViewModel.Factory>( 
        creationCallback = { factory -> 
            factory.create(key) 
        } 
    ) 
    ScreenB(viewModel = viewModel) 
}

 

🤔 メリット:100%の型安全性を実現し、SavedStateHandle のボイラープレートを完全に排除

このアーキテクチャ(Navigation 3 + @AssistedInject)に移行することで、コードベースの品質は大幅に向上します。

完全な型安全性(100% Type-Safe):
かつての文字列ベースのキー(string のキー指定)は過去のものです。ナビゲーション引数は、Route(Serializable等で定義された型)として、厳密に型指定されたオブジェクトのまま直接安全に渡されます。

SavedStateHandle からの解放 :
単に次の画面へ引数を渡すだけのために、SavedStateHandle を使ってごにょごにょと値を書き出したり読み出したりする定型文(ボイラープレート)はもう一切不要になります。

圧倒的なリファクタリングのしやすさ :
すべてがコンパイル時にチェックされるため、引数の追加・削除・変更といったリファクタリングが、一瞬かつ確実に(ランタイムエラーの心配なく)行えるようになります。

 

🤔 まとめ

Hilt と最新の Jetpack Navigation の相乗効果により、画面間の引数の受け渡しは驚くほどスムーズかつ洗練されたものになりました。もはや、型安全性を犠牲にしたり、不自然なアーキテクチャで回避策(ワークアラウンド)を講じたりする必要はありません。

@AssistedInject と、進化した hiltViewModel() API を組み合わせることで、「完璧な型安全性」と「クリーンなハイブリッド依存性注入(DI)」を今すぐあなたのプロジェクトに導入できます。