State HoistingはJetpack Composeのパターンで、状態を子Composable関数から親に移動し、子はパラメーターを介してデータを受け取り、コールバックを介して変更を通知します。これは単方向データフロー(UDF)の原則の実装であり、状態は上に引き上げられ、イベントは下に流れます。Google Android Developers、2026によると、State Hoistingはコンポーネントを再利用可能、テスト可能、予測可能にします。
主要ポイント
State Hoistingは、Composable関数が状態を所有せずに外部から受け取るパターンです。関数内でvarを使用する代わりに、表示用の値と変更を処理するラムダコールバックの2つのパラメーターを使用します。技術的には、子コンポーネントはステートレス(独自の状態を持たない)になり、親はステートフル(状態を所有する)になります。
例:Material3のTextFieldコンポーネントは、入力されたテキストを内部的に保存しません。value: StringとonValueChange: (String) -> Unitを受け入れます。TextFieldを呼び出す親は、var value by remember { mutableStateOf("") }を宣言し、valueとonValueChangeを渡します。これは典型的なState Hoistingです。TextFieldはダムコンポーネント(単に表示して入力を報告する)であり、親がスマート(状態を所有する)です。
ステートレス vs ステートフル:ステートレスコンポーネントはテストが容易です—内部状態に依存せず、その動作は入力パラメーターによって完全に決定されます。ステートフルコンポーネントは迅速なプロトタイピングに便利ですが、再利用が難しく、単一のデータソースに強く結合されています。State Hoistingは選択肢を提供します:状態を上に移動することで、任意のコンポーネントをステートレスにできます。
UDF(単方向データフロー)は、データが一方向に移動するアーキテクチャ原則です:真実のソース(ViewModelまたは親Composable)からUIへ、イベントは逆方向に流れます。State Hoistingは個々のコンポーネントレベルでのUDFの実装です。各コンポーネントがいつどのように自身の状態を変更するかを決定する代わりに、イベントについて親に通知し、親が状態の変更方法を決定します。
UDFの利点:予測可能性— 状態は一箇所でのみ変更され、競合状態を排除します;トレーサビリティ— コールスタックにより変更の連鎖を再構築できます;テスト— ステートフルなロジックを別のクラスに抽出し、UIなしでテストできます。大規模プロジェクトでは、State Hoistingと組み合わせたUDFが事実上の標準です。
単一真実ソース(Single Source of Truth)はUDFに付随するもう一つの原則です。状態の各部分には正確に一つのソースがあります。2つのコンポーネントが同じ状態を使用する場合、ソースは共有されるべきです(ViewModelまたは共通の親レベルで)。State Hoistingは、ソースが階層の上位にあり、状態の重複が発生しないことを保証します。
| 方向 | 渡されるもの | 実装方法 |
|---|---|---|
| 下(親 → 子) | 表示する値 | パラメーター value: T |
| 上(子 → 親) | 変更イベント | パラメーター onValueChange: (T) -> Unit |
基本ルール:状態は、それを必要とするすべてのコンポーネントにとって十分な最小限のレベルに引き上げられるべきです。状態が1つのコンポーネント内でのみ使用される場合は—ローカルに保ちます。隣接する2つのコンポーネントが同じ状態を必要とする場合は—共通の親に引き上げます。状態が画面全体で必要な場合は—ViewModelに引き上げます。
最小引き上げルールは、不必要な複雑さを防ぎます。テキストフィールドの状態が1つの画面内でのみ使用され、Activityの再作成時に永続化されない場合、ViewModelに引き上げる意味はありません。画面の回転を生き残るべきだがビジネスロジックに不要なUI状態には、ViewModelではなく画面の親レベルでrememberSaveableを使用します。
ViewModelに引き上げるタイミング:状態がActivityの再作成後も存続する必要がある場合、複数の画面で必要とされる場合、状態の変更がビジネスロジック(ネットワークリクエスト、データベース)をトリガーする場合。ViewModelレベルでのState HoistingはMVVMアーキテクチャにおける標準パターンであり、UI層はステートレスでViewModelがステートフルです。
// ❌ 悪い:コンポーネントが自身の状態を所有
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ 良い:State Hoisting — 状態は親に
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// 使用法:親が状態を所有
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
2つのフィールド(メール、パスワード)とボタンを持つログイン画面を考えます。3つのコンポーネントすべてがState Hoistingを介して状態を受け取ります:メールとパスワードは親によって管理され、ボタンは有効状態を値として受け取ります。
// 画面レベルでのState Hoisting
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// メールフィールド — ラムダ経由のState Hoisting
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// パスワードフィールド — 同様に
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// ボタン — enabledのみを受け取る(読み取り専用)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// ステートレスコンポーネント:メール + コールバックを受け取る
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("メール") },
singleLine = true
)
}
// ステートレスボタンコンポーネント
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("ログイン")
}
}
EmailFieldとPasswordFieldは完全にステートレスです。任意のデータソースに接続して、任意の画面で再利用できます。LoginButtonはenabledを読み取り専用として受け取ります—これは状態が引き上げられず(ボタンは自身を有効にできない)、既に準備された状態で渡される別の形式のState Hoistingです。このアプローチは、最小限のコンポーネント結合で最大の柔軟性を提供します。
すべての状態を引き上げる必要はありません。ローカル状態(Composable内のState)は次の場合に正当化されます:データが1つのコンポーネント内でのみ必要で、兄弟要素に影響を与えず、特定のセクションの再コンポジション後も存続する必要がない場合。例えば、アニメーション状態、入力フィールドのフォーカス、現在のスクロール位置—これらはローカルに保つことが合理的です。
State Hoistingが必要な場合:状態が複数の子コンポーネントによって使用される;ある子での変更が別の子に反映される必要がある;状態変更のロジックをUIとは別にテストする必要がある;状態がActivityの再作成後も存続する必要がある。これらの場合、ローカル状態はデータの重複と不整合を引き起こします。
ハイブリッドアプローチ:最小限の状態をローカルに保ち、残りを引き上げます。Composeのルール:“必要な限り高く、可能な限り低く状態を引き上げます。”実際には、ローカルのrememberから始め、別のコンポーネントからのアクセスが必要になった場合にのみレベルを引き上げます。State Hoistingを予防的に適用しないでください—不必要にコードを複雑にします。
よくある質問
State HoistingはUIコンポーネントレベルのパターンです。ViewModelはビジネスロジックのためのアーキテクチャ層です。State Hoistingは状態を親Composableレベル、画面レベル、またはViewModelに引き上げることができます。ViewModelは、Activityの再作成後も存続する必要がある状態のための最上位の引き上げポイントです。
ステートレスコンポーネントは、単に値を渡してテストします。必要なパラメーターでComposableを呼び出し、ComposeTestRuleを使用して表示を確認します。状態の変更は親またはViewModelレベルでテストされます—UIとは別に。これによりテストが大幅に簡素化されます:コンポーネント内での再コンポジションをシミュレートする必要はありません。
はい、これは一般的なプラクティスです。コンポーネントが変更能力なしにデータを表示するだけの場合は、State<T>(読み取り専用)を渡します。コンポーネントは変更を購読しますが、変更を開始することはできません。これによりカプセル化が強化され、データを望まない変更から保護します。
深い受け渡しにはCompositionLocalを使用するか、親Composableパラメーターを介して渡します。状態が画面全体で必要な場合は—ViewModelに抽出し、collectAsState()を使用します。5+レベルを介した受け渡しは、誤ったアーキテクチャの兆候です;コンポーネント階層を再検討してください。
State Hoistingは、親の変更がすべての子を再コンポーズする可能性があるため、再コンポジションの数をわずかに増やす可能性があります。変更をフィルタリングするにはderivedStateOfを、ターゲットを絞った更新にはLazyColumnのkeysを使用してください。ほとんどのシナリオでは、State Hoistingのオーバーヘッドは保守性の利点と比較して無視できます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。