Google推奨 MVVM と MVI の違い

Android の UI アーキテクチャでは、MVVM と MVI のどちらを選ぶべきかという議論がよくあります。

ただし、現在の Google の公式ガイドを見ると、重要なのは「MVVM か MVI か」という名前ではありません。

Google が推奨している中心的な考え方は、UDF(Unidirectional Data Flow), UI State・State Holder, ViewModel です。

その上で、MVI はより厳密に「すべての入力を Action として集約し、Reducer で State を生成する」方向へ進めたアーキテクチャとして考えることができます。

 

🧑🏻‍💻 1. Input Processing — Event の処理

MVVM: ViewModel のメソッドを直接呼び出す

UI から発生したイベントごとに、対応する ViewModel のメソッドを呼び出します。


Button(
    onClick = { viewModel.onSubmitClicked() }
)


fun onSubmitClicked() {
    viewModelScope.launch {
        ...
    }
}

このスタイルでは、UI イベントと ViewModel の API が直接対応します。

Google の Compose アーキテクチャガイドでも、UI イベントを ViewModel の onSignIn() のようなメソッドで処理する例が示されています。

MVI: Single Entry Point

MVI では、UI からの入力を Intent や Action に変換し、1つの入口に集約することがあります。


Button(
    onClick = { viewModel.onIntent(UserIntent.Submit) }
)


sealed interface UserIntent {
    data object Submit : UserIntent
    data object Refresh : UserIntent
}

fun onIntent(intent: UserIntent) {
    ...
}

これにより、UI → ViewModel の入力経路を1本化できます。

すべての入力を同じ形式で扱えるため、ログやテスト、イベント追跡を統一しやすくなります。

 

🧑🏻‍💻 2. State Reduction — State の更新

MVVM: ViewModel の処理から State を更新する

MVVM では、ViewModel の各メソッドが非同期処理を行い、その結果を UiState に反映する構成が一般的です。


fun loadData() {
    viewModelScope.launch {
        val result = repository.getData()

        _uiState.update {
            it.copy(data = result)
        }
    }
}

この場合、State を変更する場所は ViewModel 内の複数のメソッドに分散する可能性があります。

その代わり、コードが比較的シンプルで、処理の流れを直接追いやすいというメリットがあります。

MVI: Reducer で State を計算する

MVI では、現在の State と Action から次の State を計算する reduce を中心に据えることができます。


fun reduce(
    oldState: UiState,
    action: Action
): UiState =
    when (action) {
        is Action.DataLoaded ->
            oldState.copy(data = action.data)

        is Action.Loading ->
            oldState.copy(isLoading = true)
    }

ここで重要なのは、


Current State + Action
          ↓
       Reducer
          ↓
      Next State

という形にすることです。

Reducer を純粋関数として設計すれば、同じ State と Action から常に同じ State を生成するという性質を持たせられます。

そのため、State 遷移のテストやログの再現が容易になります。

 

🧑🏻‍💻 3. Output Processing — UI 更新と Side Effect

MVVM: State を中心に UI を更新する

Google の現在の Android アーキテクチャガイドは、ViewModel が UI State を生成し、UI がそれを購読する UDF を推奨しています。


val uiState: StateFlow<UiState> =
    _uiState.asStateFlow()

val state by viewModel.uiState
    .collectAsStateWithLifecycle()

例えば Snackbar を UI State に含める実装では、UI が State の変化を監視して表示できます。


LaunchedEffect(state.userMessage) {
    state.userMessage?.let {
        snackbarHostState.showSnackbar(it)
        viewModel.onMessageShown()
    }
}

この方式では、


ViewModel
    ↓
 UiState
    ↓
   UI

という一本の State Flow に寄せることができます。

なお、「一度だけ実行したいイベントを State に入れるべきか」については、現在の Google のガイドラインでは以前より明確に State 中心へ寄っています。

Google は ViewModel から UI へ直接イベントを送るのではなく、イベントの結果を UI State に反映することを推奨しています。

MVI: State と Effect を分離する

一方、MVI では永続的な UI State と、一度だけ実行したい Side Effect を分離する設計も一般的です。


private val _effect = Channel<UiEffect>()

val effect = _effect.receiveAsFlow()

val state by viewModel.state
    .collectAsStateWithLifecycle()

UI 側では、


LaunchedEffect(Unit) {
    viewModel.effect.collect { effect ->
        when (effect) {
            is UiEffect.ShowSnackbar ->
                snackbarHostState.showSnackbar(
                    effect.message
                )
        }
    }
}

のように State と Effect を別々に処理します。


      ┌───────────┐
      │ ViewModel │
      └─────┬─────┘
            │
   ┌────────┴────────┐
   ↓                 ↓
UiState           UiEffect
   ↓                 ↓
  UI            Side Effect

この設計のメリットは、「画面を表現する State」と「UI に一度だけ何かをさせる Effect」の責務を明確に分離できることです。

一方で、Channel や SharedFlow、Effect の lifecycle や delivery guarantee まで設計する必要があり、複雑さも増えます。

 

🧑🏻‍💻 4. MVVM と MVI の違いを整理する

 

🧑🏻‍💻 5. 「MVVM vs MVI」ではなく「どこまで厳密にするか」

ここで重要なのは、MVVM と MVI は完全に対立するものではないということです。


              UDF
               │
     ┌─────────┴─────────┐
     ↓                   ↓
   MVVM                  MVI
     │                   │
ViewModel          Intent / Action
     │                   │
UI State             Reducer
     │                   │
     └─────────┬─────────┘
               ↓
              UI

Google の公式ガイドが推奨しているのは、MVI という名前ではなく、UDF によって UI State を一方向に流し、ViewModel などの State Holder がユーザーイベントを処理する構造です。

そのため、ViewModel に複数のイベントハンドラを持たせること自体が「悪い MVVM」なのではありません。

むしろ Google の公式サンプルでも、onSignIn() のようなイベントハンドラを ViewModel に公開する形が使われています。

MVI はそこからさらに一歩進めて、

UI Event


   ↓
Intent / Action
   ↓
Reducer
   ↓
UiState
   ↓
UI

という形に統一することで、状態遷移そのものをアーキテクチャの中心に置く考え方だと言えます。

 

🧑🏻‍💻 6. どちらを選ぶべきか

シンプルな画面

- MVVM + UDF が適しています。
- ViewModel のメソッドを直接呼び出すだけで十分です。

状態遷移が複雑な画面

- MVI の考え方が有効です。
- Action と Reducer を明確に分離することで、状態遷移を追いやすくできます。

大規模チーム

- MVI のような厳密なルールが、チーム内の実装パターンを統一する助けになる場合があります。
- ただし、Reducer や Intent、Effect を増やすこと自体が目的になってはいけません。

重要なのはアーキテクチャの名前ではありません。

- State をどこに置くのか。
- 誰が State を変更するのか。
- Event をどこで処理するのか。
- UI に何を State として公開するのか。
- UI Effect をどのように扱うのか。

これらを明確にすることが、MVVM と MVI を選択する本質です。

 

🧑🏻‍💻 まとめ

MVVM は、少ないコードで UDF を実現しやすい実用的な選択肢です。

MVI は、Intent → Action → Reducer → State という流れを厳密に定義することで、State 遷移の予測可能性を高めます。

Google の現在の公式 Android ガイドは、MVI を推奨しているわけではなく、UDF + UI State + State Holder + ViewModel を中心に説明しています。

したがって、現代の Android では、


MVVM
  ↓
UDF + State
  ↓
必要なら MVI 的な Reducer / Action を導入

という考え方のほうが、「MVVM か MVI か」という二択よりも実際の設計に近いと言えるでしょう。


2026年8月31日 API レベル36+ の件

もう疲れました。

Google PlayアプリのターゲットAPIレベル要件
2026年8月31日から開始:

・ Google Playに提出される新規アプリおよびアプリのアップデートは、Android 16(APIレベル36)以降をターゲットにする必要があります。ただし、Wear OSおよびAndroid Automotive OSアプリはAndroid 15(APIレベル35)以降をターゲットに、Android TVおよびAndroid XRアプリはAndroid 14(APIレベル34)以降をターゲットにする必要があります。

・既存のアプリは、アプリのターゲットAPIレベルよりも高いAndroid OSを実行しているデバイスで新規ユーザーが引き続き利用できるようにするには、Android 15(APIレベル35)以上をターゲットにする必要があります。Android 14(APIレベル34)以下をターゲットとするアプリ(Wear OSおよびAndroid TV向けのAndroid 13(APIレベル33)以下、Android XR向け、Android Automotive OS向けのAndroid 12(APIレベル31)以下を含む)は、アプリのターゲットAPIレベルと同じかそれ以下のAndroid OSを実行しているデバイスでのみ利用可能です。

アプリのアップデートにさらに時間が必要な場合は、2026年11月1日まで延長を申請できます 。アプリの延長申請フォームは、今年後半にPlay Consoleからアクセスできるようになります。

👉 Target API level requirements for Google Play apps - Play Console Help

実質のユーザ利用の端末OSレベルが API 36+ で 22.3% ということなのですが。

アプリ開発側だけが辛い感じで。

みなさんは、どう思ってますか。


OkHttp・Retrofit等の主要ライブラリ、コントリビューションへのLLM使用を禁止方針に

View on Mastodon

本日より、Lysineプロジェクト(OkHttp、Okio、Retrofit、SQL Delightファミリー)では、コードの作成、およびPR(プルリクエスト)/ Issueの記述やコメントにおいてLLM(大規模言語モデル/生成AI)の使用を禁止するコントリビューションポリシーを導入します。

ただし、利用を明示することを条件に、翻訳補助としての使用は例外として認められます。

リサーチ、分析、あるいは実験の目的でローカル環境でLLMを使用されることについて、私たちがそれを止めることはできません。しかし、プロジェクトへ貢献する際にはあなた自身がコードを書き、やり取りの相手もあなた自身であることを要求します。

 

🤔 まとめ

対象プロジェクト:
OkHttp、Okio、Retrofit、SQL Delightシリーズなど(Lysine配下のプロジェクト)

禁止事項:
提出コードの作成、PR/Issueの本文やコメントにおけるLLM(生成AI)の使用

許可される例外:
翻訳補助としての利用(ただし使用した旨の明記が必要)

個人での利用:
手元での調査・分析・実験的なコード試作におけるLLM使用は制約外

コアメッセージ:
コントリビューションにおいては「AIではなく開発者本人が直接書くこと」および「人間同士で対話すること」を重視する方針

そもそも、LLMで勘違いしてるやつ、多いですよね?


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 を使うのをやめるべきなのか?

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

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

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

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

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

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

 

🤔 参考


Navigation 3 の Navigation Graph パターンまとめ

 

🤔 Navigation Graph はもう navigation() だけじゃない

Jetpack Compose 向けの Navigation 3 では、従来の Navigation Component にあった navigation() を使ったネストした Navigation Graph の考え方がなくなりました。

その代わりに、「コードをどう整理するか」と「BackStack をどう分けるか」を分離して考える設計になっています。

この記事では、Navigation 3 でよく使われる 3 つの Navigation Graph パターンを紹介します。

 

🤔 Navigation 3 の3つのパターン


Navigation 3
│
├─ コードを整理したい
│   ├─ Extension Functions
│   └─ Sealed NavKeys
│
└─ BackStack を分けたい
    └─ Nested NavDisplay

ポイントは、

- コードを整理する方法
- BackStack を分離する方法

は別の話だということです。

 

🤔 Extension Functions で画面を機能ごとに分割する

Navigation 3 では entry() を並べて画面を登録します。

画面数が増えてくると、1つのファイルに全部書くのは見づらくなります。

そんなときは Extension Function へ切り出すのが最もシンプルです。


// AuthModule.kt
fun EntryProviderScope<NavKey>.authGraph() {
    entry<Login> {
        LoginScreen()
    }

    entry<SignUp> {
        SignUpScreen()
    }
}

メイン側では呼び出すだけです。


val entryProvider = entryProvider {
    authGraph()

    entry<Home> {
        HomeScreen()
    }
}

NavDisplay(
    backStack = backStack,
    entryProvider = entryProvider
)

これだけで認証画面を別ファイルへ分離できます。

メリット
- Feature 単位で管理できる
- モジュール分割しやすい
- BackStack は1つなので構成はシンプル

 

🤔 Sealed NavKey で画面をグループ化する

もう1つの整理方法が NavKey をグループ化する 方法です。


sealed interface AuthKey : NavKey {

    @Serializable
    data object Login : AuthKey

    @Serializable
    data object SignUp : AuthKey
}

sealed interface MainKey : NavKey {

    @Serializable
    data object Home : MainKey

    @Serializable
    data class Detail(
        val id: String
    ) : MainKey
}

画面遷移も名前空間付きで分かりやすくなります。

Navigation 3 では Graph は存在しませんが、

- Auth 系
- Main 系
- Settings 系

のように論理的に整理できます。

メリット
- 名前空間が分かりやすい
- 型安全
- Destination が探しやすい

 

🤔 NavDisplay をネストする

ここだけは少し意味が変わります。

これはコード整理ではなく BackStack を分離する方法です。

例えば

- Bottom Navigation
- タブ
- Onboarding
- 独立したフロー

では、それぞれ独自の履歴を持たせたいことがあります。

その場合は NavDisplay をネストします。


entry<MainTabs> {
    MainTabsScreen()
}


@Composable
fun MainTabsScreen() {

    val tabBackStack =
        rememberNavBackStack(HomeTab)

    NavDisplay(
        backStack = tabBackStack,
        entryProvider = entryProvider {

            entry<HomeTab> {
                HomeScreen()
            }

            entry<SearchTab> {
                SearchScreen()
            }

            entry<ProfileTab> {
                ProfileScreen()
            }
        }
    )
}

ここでは親とは別の NavBackStack を持っています。


Root 
NavDisplay
    │
    └── MainTabs
             │
             └── NavDisplay
                     │
                     ├── Home
                     ├── Search
                     └── Profile

つまり、

NavDisplay = NavBackStack の管理単位

という考え方になります。

Bottom Navigation で各タブの履歴を保持できるのも、この仕組みによるものです。

 

🤔 どれを選べばいい?

次のように考えると分かりやすいでしょう。


画面を整理したい?
│
├─ Yes
│   ├─ Extension Functions
│   └─ Sealed NavKeys
│
└─ BackStack を分けたい?
    └─ Nested NavDisplay

 

🤔 まとめ

Navigation 3 では、従来の navigation() による Graph のネストはありません。

その代わりに、

- Extension Functions で entry() を機能ごとに分割する
- Sealed NavKey で Destination を整理する
- Nested NavDisplay で独立した NavBackStack を持つ

という3つのパターンを組み合わせて設計します。

特に重要なのは、コードの整理と BackStack の分離は別の概念という点です。

Navigation 3 は Graph をネストするライブラリではなく、NavDisplay と NavBackStack を組み合わせてナビゲーション構造を組み立てるライブラリへと進化しています。

 

🤔 参考