UI Action は UiState を直接変更しない

UiState は入力から導出する

UI = f(State) は、宣言的 UI の基本的な考え方です。

では、UIからのActionによってStateをどう変えるのでしょうか?

ルールはシンプルです。

UI Action から public な UiState を直接変更しない。

UI Action は ViewModel が持つ private な入力を更新します。

そして、UiState はその入力から導出します。


UI Action
  ↓
Private Flow
  ↓
UiState
  ↓
UI

例えば、検索画面なら次のように書けます。


private val _query = MutableStateFlow("")

val uiState = combine(
    _query,
    repository.items
) { query, items ->
    UiState(
        query = query,
        items = items
    )
}

fun onQueryChanged(query: String) {
    _query.value = query
}

次のように UiState を直接変更することもできます。


fun onQueryChanged(query: String) {
    _uiState.update {
        it.copy(query = query)
    }
}

この場合、UI Action 自身が画面の State を管理することになります。

一方、private な入力を更新するだけなら、責務が明確になります。


UI Action
    ↓
入力を変更する
    ↓
UiState を導出する
    ↓
UI を描画する

つまり、


UiState = f(Inputs)

であり、


UI Action → UiState を変更する

ではありません。

UI Action の役割は、「何が起きたか」を ViewModel に伝えることです。

そして、「その結果どのようなUiStateになるか」は Flow の依存関係によって決まります。

UI Action は UiState を変更する命令ではなく、入力を変更するトリガーである。

 

🧑🏻‍💻 参考


State は宣言的に、ユーザーの操作は命令的に

 

🤔 ViewModelで「Stateの生成」と「ユーザー操作」を分けて考える

Jetpack Composeでは、UIを宣言的に書くことが当たり前になりました。

「UIをどう更新するか」を細かく指示するのではなく、

「このStateなら、UIはこう見える」

という関係をコードで表現します。

この考え方は、ViewModelにもそのまま応用できます。

私はViewModelを書くとき、次のシンプルなルールで考えると整理しやすいと思っています。

・ Stateの生成は宣言的に。
・ ユーザーの意図への反応は命令的に。

この2つを分けるだけで、ViewModelはかなり読みやすくなります。

 

🤔 State は宣言的に生成する

たとえば、Repositoryがユーザー一覧をFlowとして公開しているとします。


Repository
    │
    │ users: Flow<List<User>>
    ▼
   map
    │
    ▼
 UiState
    │
    ▼
 Compose

この場合、ViewModelではFlowからUiStateをそのまま導出できます。


class UserViewModel(
    repository: UserRepository
) : ViewModel() {

    val uiState: StateFlow<UiState> =
        repository.users
            .map { users ->
                UiState.Success(users)
            }
            .stateIn(
                viewModelScope,
                SharingStarted.WhileSubscribed(5_000),
                UiState.Loading
            )
}

ここには init がありません。

load() もありません。

ViewModel がやっているのは、

uiState は repository.users から導出される

という関係を定義することだけです。

Repository から新しい値が流れてくれば、それに応じて uiState も変わります。

ここには「最初にこれを実行して、そのあとこれを実行する」という手順がありません。

あるのは、


users → UiState

という関係だけです。

これが「宣言的な State 生成」です。

 

🤔 命令的なコードにも、もちろん役割がある

だからといって、命令的なコードをなくす必要はありません。

たとえば「更新」ボタンを考えてみます。


fun refresh() {
    viewModelScope.launch {
        repository.refresh()
    }
}

これは自然なコードです。

ユーザーが「更新したい」と明示的に操作したからです。

処理の流れも明確です。


ユーザーがRefreshをタップ
        ↓
     refresh()
        ↓
Repositoryが更新処理を実行
        ↓
     Stateが変化

これは命令的な処理です。

そして、こういう場所こそ命令的なコードが適しています。

同じことは、たとえば次のような操作にも当てはまります。


fun retry()
fun deleteUser(id: String)
fun submit()

これらはすべて、

「ユーザーがこうしてほしい」

という意図を表しています。

そのため、命令として表現するのが自然です。

 

🤔 問題は、この2つを混ぜてしまうこと

少し気になるのは、State を生成するためだけに命令的なコードを使うケースです。

たとえば、こんな ViewModel です。


class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    val uiState: StateFlow<UiState> = ...

    init {
        load()
    }

    private fun load() {
        viewModelScope.launch {
            repository.loadUsers()
        }
    }
}

このコード自体が間違っているわけではありません。

ただ、ここで一度考えてみたいことがあります。

なぜ load() が必要なのでしょうか?

すでに uiState を宣言的に定義しているのに、その State を作るために別の命令を実行しなければなりません。

構造としては、こうなっています。



ViewModelの生成
      ↓
    init
      ↓
    load()
      ↓
 Stateが生成・更新

つまり、State を得るために「いつ load() を呼ぶか」という別の問題が発生します。

もし Repository がデータを Flow として公開できるのであれば、この命令自体が不要になるかもしれません。

 

🤔 LaunchedEffect に移しても、本質は変わらない

「それなら load() を Compose 側から呼べばいい」と考えることもできます。


@Composable
fun UserScreen(
    viewModel: UserViewModel
) {
    LaunchedEffect(Unit) {
        viewModel.load()
    }

    // UI
}

UI そのものは宣言的です。

しかし、ここでは別の命令が追加されています。

「この画面がCompositionに入ったらload()を呼ぶ」

つまり、


Screen enters Composition
          ↓
   LaunchedEffect
          ↓
       load()
          ↓
      State changes

という流れです。

もちろん、これは常に悪いわけではありません。

明示的な副作用が必要なケースでは、LaunchedEffect は適切な選択です。

ただし、

「Flowとして表現できるStateを最初に作るためだけ」

に LaunchedEffect を使っているのであれば、一度設計を見直してみる価値があります。

 

🤔 ViewModel の init も同じ視点で考える

ここで、ViewModel の init についても考えてみます。


init {
    load()
}

これは、

「ViewModelが生成されたら、この処理を開始する」

という命令です。

処理の順番は明確です。


ViewModelが生成される
        ↓
       init
        ↓
      load()
        ↓
    Stateが変化

一方、State を宣言的なパイプラインとして表現できるなら、


Repository State
       ↓
      map
       ↓
     UiState

と直接書けます。

ここでは「いつ State を作るか」をコードで指示していません。

State がどのように導出されるかだけを記述しています。

この違いは小さく見えますが、ViewModel の責務を考えるうえで重要です。

 

🤔 すべてを宣言的にする必要はない

ここが一番重要なポイントかもしれません。

この考え方は、

「ViewModelの処理を全部Flowにしよう」

という話ではありません。

たとえばユーザーが削除ボタンを押した場合、


fun deleteUser(id: String) {
    viewModelScope.launch {
        repository.deleteUser(id)
    }
}

で何も問題ありません。

ユーザーが「このユーザーを削除したい」と意図を示したので、その意図を命令的な処理として表現するのは自然です。

重要なのは、

・ すべてを宣言的にすること

ではありません。

むしろ、

・ Stateは宣言的に。
・ ユーザーの意図は命令的に。

という境界を持つことです。

境界があると、ViewModel のコードを読むときにも、

・ Stateを作っているコードなのか
・ ユーザーの操作に反応しているコードなのか

を簡単に区別できます。

 

🤔 init { load() } を疑ってみる

この考え方をすると、これまで何となく書いていたコードも少し違って見えてきます。


init {
    load()
}

あるいは、


LaunchedEffect(Unit) {
    viewModel.load()
}

を見たとき、

「初期ロードだから、とりあえずこれを書く」

ではなく、

「そもそも、この load() は必要なのだろうか?」

と考えられるようになります。

もちろん、必要な場合もあります。

外部 API を明示的に叩く必要があったり、ユーザーの操作によって処理を開始する必要があったりするなら、命令的な処理は適切です。

ただ、State そのものが Flow から自然に導出できるのであれば、State を作るためだけの load() は必要ないかもしれません。

 

🧑🏻‍💻 まとめ

宣言的プログラミングと命令的プログラミングのどちらかを選ぶ必要はありません。

ViewModelの中でも、それぞれに適した場所があります。


State Generation
      ↓
  Declarative

Flow → map → UiState


User Intent
      ↓
  Imperative

大切なのは、この2つを混ぜないことです。

State の生成は宣言的に。
ユーザーの意図への反応は命令的に。

この境界を意識すると、init { load() } や LaunchedEffect { viewModel.load() } を「お決まりの初期化パターン」として使うのではなく、

本当にこの load() は必要なのか?

と考えられるようになります。

そして、ときには初期化問題を解決する最善の方法は、load() を別の場所に移動することではありません。

そもそも load() が必要な設計なのかを見直すことです。

 

🧑🏻‍💻 参考記事


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で勘違いしてるやつ、多いですよね?