アプリにおいて状態とは、時間とともに変化する可能性がある値すべてを意味します。これは非常に広範な定義であり、Room データベースにも、クラス内の変数一つにも当てはまります。
すべての Android アプリはユーザーに状態を表示します。Android アプリの状態の例を次にいくつか示します。
- ネットワーク接続を確立できないときに表示されるスナックバー。
- ブログ投稿と関連コメント。
- ユーザーがボタンをクリックしたときに再生されるボタンの波紋アニメーション。
- ユーザーが画像の上に描画できるステッカー。
Jetpack Compose では、Android アプリが状態をどこで、どのように保存し、使用するかの設定を明示的に行うことができます。
UI 更新ループとイベント
Android アプリでは、イベントに応じて状態が更新されます。イベントとはアプリ外で生成された入力であり、OnClickListener
を呼び出すボタンをユーザーがタップする操作、EditText
による afterTextChanged
の呼び出し、加速度計による新しい値の送信などがあります。
すべての Android アプリは、次のようなコア UI 更新ループを備えています。
- イベント: ユーザーまたはプログラムの要素によってイベントが生成されます。
- 状態の更新: イベント ハンドラが状態を変更します。
- 状態の表示: UI が更新され、新しい状態を表示します。
Jetpack Compose では、状態とイベントは分離されています。状態は変更可能な値を表し、イベントは何かが起こったことの通知を表します。
状態をイベントから分離することで、状態の保存と変更の仕組みから状態の表示を切り離すことが可能になります。
Jetpack Compose の単方向データフロー
Compose は、単方向データフローを使用するように設計されています。この設計では、状態は下方に流れ、イベントは上方に流れます。

単方向データフローに従うことで、UI に状態を表示するコンポーザブルと、状態を保存および変更するアプリの要素を切り離すことができます。
単方向データフローを使用するアプリの UI 更新ループは、次のようになります。
- イベント: UI の要素によってイベントが生成され、上位に渡されます。
- 状態の更新: イベント ハンドラが状態を変更します。
- 状態の表示: 状態が下位に渡され、UI が新しい状態を観測して表示します。
Jetpack Compose を使用する際にこのパターンに従うと、次のような利点があります。
- テストの容易性: 状態を表示する UI から状態を切り離すことで、両者を分離して簡単にテストできます。
- 状態のカプセル化: 状態が 1 か所でのみ更新されるため、状態の不整合(バグ)が生じる可能性が低くなります。
- UI の整合性: オブザーバブルな状態ホルダーを使用することにより、すべての状態の更新が UI に即座に反映されます。
ViewModel と単方向データフロー
Android アーキテクチャ コンポーネントの ViewModel
と LiveData
を使用する場合は、アプリに単方向データフローを導入します。
Compose で ViewModel
を参照する前に、Android ビューを使用する Activity
と、"Hello, ${name}"
を表示してユーザーが名前を入力できるようにする単方向データフローを使用することを検討してください。
この画面のコードでは ViewModel
と Activity
を使用しています。
class HelloViewModel : ViewModel() {
// LiveData holds state which is observed by the UI
// (state flows down from ViewModel)
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
// onNameChanged is an event we're defining that the UI can invoke
// (events flow up from UI)
fun onNameChanged(newName: String) {
_name.value = newName
}
}
class HelloActivity : AppCompatActivity() {
val helloViewModel by viewModels<HelloViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// binding represents the activity layout, inflated with ViewBinding
binding.textInput.doAfterTextChanged {
helloViewModel.onNameChanged(it.toString())
}
helloViewModel.name.observe(this) { name ->
binding.helloText.text = "Hello, $name"
}
}
}
Android アーキテクチャ コンポーネントを使用することで、この Activity
に単方向データフロー設計を導入しています。

ViewModel
を使用する Activity
での単方向データフローUI 更新ループで単方向データフローがどのように動作するかを確認するには、この Activity
のループをチェックしてください。
- イベント: テキスト入力が変更されると、UI によって
onNameChanged
が呼び出されます。 - 状態の更新:
onNameChanged
が処理を行った後、_name
の状態を設定します。 - 状態の表示:
name
のオブザーバーが呼び出され、UI が新しい状態を表示します。
ViewModel と Jetpack Compose
前のセクションの Activity
の場合と同様に、Jetpack Compose の LiveData
と ViewModel
を使用して単方向データフローを実装できます。
同じ HelloViewModel
を使用して Jetpack Compose で記述された HelloActivity
と同じ画面のコードを次に示します。
class HelloViewModel : ViewModel() {
// LiveData holds state which is observed by the UI
// (state flows down from ViewModel)
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
// onNameChanged is an event we're defining that the UI can invoke
// (events flow up from UI)
fun onNameChanged(newName: String) {
_name.value = newName
}
}
@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
// by default, viewModel() follows the Lifecycle as the Activity or Fragment
// that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen.
// name is the _current_ value of [helloViewModel.name]
// with an initial value of ""
val name: String by helloViewModel.name.observeAsState("")
Column {
Text(text = name)
TextField(
value = name,
onValueChange = { helloViewModel.onNameChanged(it) },
label = { Text("Name") }
)
}
}
HelloViewModel
と HelloScreen
は、単方向データフロー設計に従います。状態は HelloViewModel
から下方に流れ、イベントは HelloScreen
から上方に流れます。
このコンポーザブルの UI イベントループについて考えてみましょう。
- イベント: ユーザーが文字を入力すると、それに応じて
onNameChanged
が呼び出されます。 - 状態の更新:
onNameChanged
が処理を行った後、_name
の状態を設定します。 - 状態の表示:
name
の値が変更され、それが Compose によってobserveAsState
で観測されます。次に、HelloScreen
が再実行(再コンポーズ)され、name
の新しい値に基づいて UI を記述します。
ViewModel
と LiveData
を使用して Android で単方向データフローを構築する方法の詳細については、アプリ アーキテクチャのガイドをご覧ください。
ステートレスなコンポーザブル
ステートレスなコンポーザブルとは、状態そのものを変更できないコンポーザブルです。ステートレスなコンポーザブルはテストが簡単で、通常はバグが少なく、再利用の機会を数多く提供します。
コンポーザブルに状態がある場合は、状態ホイスティングを使用してステートレスにすることができます。状態ホイスティングとは、コンポーザブルの内部状態をパラメータとイベントで置き換えることにより、状態をコンポーザブルの呼び出し元に移動するプログラミング パターンです。
状態ホイスティングの例を確認するには、HelloScreen
からステートレスなコンポーザブルを抽出します。
@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
// helloViewModel follows the Lifecycle as the Activity or Fragment that calls this
// composable function. This lifecycle can be modified by callers of HelloScreen.
// name is the _current_ value of [helloViewModel.name]
val name: String by helloViewModel.name.observeAsState("")
HelloInput(name = name, onNameChange = { helloViewModel.onNameChanged(it) })
}
@Composable
fun HelloInput(
name: String, /* state */
onNameChange: (String) -> Unit /* event */
) {
Column {
Text(name)
TextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
HelloInput
は、不変の String
パラメータとしての状態にアクセスできます。また、状態の変更をリクエストするときに呼び出せるイベント onNameChange
としての状態にもアクセスできます。
ラムダは、コンポーザブルでイベントを記述する最も一般的な方法です。ここでは、Kotlin の関数型構文 (String) -> Unit
を使用して、String
を受け取るラムダでイベント onNameChange
を定義します。onNameChange
が現在形であることにご注意ください。これは、状態がすでに変更されたイベントではなく、コンポーザブルがイベント ハンドラに状態の変更をリクエストするイベントであることを意味します。
HelloScreen
は、name
状態を直接変更できる最終クラス HelloViewModel
への依存関係を含んでいるため、ステートフルなコンポーザブルです。HelloScreen
の呼び出し元が name
状態の更新を制御する方法はありません。HelloInput
は、状態を直接変更できないため、ステートレスなコンポーザブルです。
HelloInput
の状態をホイストすることで、コンポーザブルの使用を考慮し、さまざまな状況で再利用して、テストすることが容易になります。HelloInput
は、状態を保存する方法から切り離されています。これは、HelloViewModel
を変更または置換する場合、HelloInput
の実装方法を変更する必要がないことを意味します。
状態ホイスティングのプロセスを使用すると、単方向データフローをステートレスなコンポーザブルに拡張できます。そのようなコンポーザブルの単方向データフロー図では、状態を操作するコンポーザブルが増えても、常に状態が下方に流れ、イベントが上方に流れます。
ステートレスなコンポーザブルであっても、単方向データフローと状態ホイスティングを使用することより、時間とともに変化する状態を操作できることを理解することが重要です。
この仕組みを理解するため、HelloInput
の UI 更新ループについて考えてみましょう。
- イベント: ユーザーが文字を入力すると、それに応じて
onNameChange
が呼び出されます。 - 状態の更新:
HelloInput
は状態を直接変更できません。呼び出し元は、onNameChange
イベントに応じて状態の変更を選択できます。ここで、呼び出し元HelloScreen
はHelloViewModel
でonNameChanged
を呼び出し、それによってname
の状態が更新されます。 - 状態の表示:
name
の値が変更されると、observeAsState
が原因となって、更新されたname
でHelloScreen
が再度呼び出されます。次に、新しいname
パラメータでHelloInput
が再度呼び出されます。状態の変更に応じてコンポーザブルを再度呼び出すことを、再コンポジションと呼びます。
コンポジションと再コンポジション
Composition は UI の記述であり、コンポーザブルの実行により作成されます。Composition は、UI を記述するコンポーザブルのツリー構造です。
初回コンポジションの際に、Jetpack Compose は、Composition の UI を記述するために呼び出されたコンポーザブルをトラックキングします。その後、アプリの状態が変更されると、Jetpack Compose は再コンポジションをスケジュール設定します。再コンポジションでは、状態の変更に応じて変更される可能性があるコンポーザブルが実行され、Jetpack Compose が Composition を更新して変更を反映します。
Composition は、初回コンポジションによってのみ作成され、再コンポジションによってのみ更新されます。Composition を変更する唯一の方法は、再コンポジションを行うことです。
初回コンポジションと再コンポジションの詳細については、Compose の思想をご覧ください。
コンポーザブル内の状態
コンポーズ可能な関数は、remember
コンポーザブルを使用して、単一のオブジェクトをメモリに保存できます。初回コンポジションの際に、remember
によって計算された値が Composition に保存され、保存された値は再コンポジションの際に返されます。remember
を使用すると、可変オブジェクトと不変オブジェクトの両方を保存できます。
remember を使用して不変値を保存する
テキストの書式の計算のような負荷の高い UI オペレーションをキャッシュに保存する際に、不変値を保存できます。remember で保存される値は、remember
を呼び出したコンポーザブルにより Composition に格納されます。
@Composable
fun FancyText(text: String) {
// by passing text as a parameter to remember, it will re-run the calculation on
// recomposition if text has changed since the last recomposition
val formattedText = remember(text) { computeTextFormatting(text) }
/*...*/
}

formattedText
を子として持つ FancyText
の Compositionremember を使用してコンポーザブルの内部状態を作成する
remember
を使用して可変オブジェクトを保存する際は、コンポーザブルに状態を追加します。この方法により、単一のステートフルなコンポーザブルに内部状態を作成できます。
コンポーザブルによって使用されるすべての可変状態をオブザーバブルにすることを強くおすすめします。そうすれば、状態が変更されるたびに、Compose は自動的に再コンポーズを実行できます。Compose には、Compose ランタイムに直接統合されている組み込みのオブザーバブルな型 State<T>
が用意されています。
コンポーザブルの内部状態の良い例は、ユーザーがボタンをクリックしたときに折りたたみと展開を切り替える ExpandingCard
です。

ExpandedCard
コンポーザブルによる折りたたみと展開の切り替えこのコンポーザブルには、重要な状態 expanded
があります。コンポーザブルは、expanded
のときは本文を表示し、折りたたまれているときは本文を非表示にする必要があります。

expanded
状態を子として持つ ExpandingCard
の Compositionコンポーザブルに状態 expanded
を追加するには、remember を使用して mutableStateOf(initialValue)
を保存します。
@Composable
fun ExpandingCard(title: String, body: String) {
// expanded is "internal state" for ExpandingCard
var expanded by remember { mutableStateOf(false) }
// describe the card for the current state of expanded
Card {
Column(
Modifier
.width(280.dp)
.animateContentSize() // automatically animate size when it changes
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
) {
Text(text = title)
// content of the card depends on the current value of expanded
if (expanded) {
// TODO: show body & collapse icon
} else {
// TODO: show expand icon
}
}
}
}
mutableStateOf
はオブザーバブルな MutableState<T>
を作成します。これは、Compose ランタイムに統合されているオブザーバブルな型です。
interface MutableState<T> : State<T> {
override var value: T
}
value
を変更すると、value
を読み取るすべてのコンポーズ可能な関数の再コンポジションがスケジュール設定されます。ExpandingCard
の場合は、expanded
が変更されるたびに ExpandingCard
が再コンポーズされます。
コンポーザブルの MutableState
オブジェクトを宣言するには、次の 3 つの方法があります。
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
これらの宣言は同等であり、状態のさまざまな用途に応じて糖衣構文として提供されます。作成するコンポーザブル向けに読みやすいコードを生成する構文を選択する必要があります。
コンポーザブルの内部状態の値は、別のコンポーザブルに渡すパラメータとして使用できます。どのコンポーザブルを呼び出すかを変更するために使用することさえできます。ExpandingCard
では、expanded
の現在の値に基づいて、if ステートメントによりカードの内容が変更されます。
if (expanded) {
// TODO: show body & collapse icon
} else {
// TODO: show expand icon
}
コンポーザブルの内部状態を変更する
状態はコンポーザブル内のイベントによって変更する必要があります。イベントではなくコンポーザブルの実行によって状態を変更すると、回避すべきコンポーザブルの副作用が生じます。Jetpack Compose の副作用の詳細については、Compose の思想をご覧ください。
ExpandingCard
コンポーザブルを完了するため、expanded
が true
のときは body
と折りたたみボタンを表示し、expanded
が false
のときは展開ボタンを表示します。
@Composable
fun ExpandingCard(title: String, body: String) {
var expanded by remember { mutableStateOf(false) }
// describe the card for the current state of expanded
Card {
Column(
Modifier
.width(280.dp)
.animateContentSize() // automatically animate size when it changes
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
) {
Text(text = title)
// content of the card depends on the current value of expanded
if (expanded) {
Text(text = body, Modifier.padding(top = 8.dp))
// change expanded in response to click events
IconButton(onClick = { expanded = false }, modifier = Modifier.fillMaxWidth()) {
Icon(Icons.Default.ExpandLess)
}
} else {
// change expanded in response to click events
IconButton(onClick = { expanded = true }, modifier = Modifier.fillMaxWidth()) {
Icon(Icons.Default.ExpandMore)
}
}
}
}
}
このコンポーザブルでは、onClick
イベントに応じて状態が変更されます。expanded
はプロパティ デリゲート構文で var
を使用するので、onClick
コールバックは expanded
を直接割り当てることができます。
IconButton(onClick = { expanded = true }, /* … */) {
// ...
}
以上により、ExpandingCard
の UI 更新ループを記述して、Compose によってどのように内部状態が変更され、使用されるかを確認できるようになりました。
- イベント: ユーザーがいずれかのボタンをタップすると、それに応じて
onClick
が呼び出されます。 - 状態の更新: 割り当てを使用して、
onClick
リスナーでexpanded
が変更されます。 - 状態の表示:
expanded
が変更されたState<Boolean>
となり、ExpandingCard
がif(expanded)
行でそれを読み取るため、ExpandingCard
が再コンポーズされます。次に、ExpandingCard
がexpanded
の新しい値に対応する画面を記述します。
Jetpack Compose で他の型の状態を使用する
Jetpack Compose では、状態を保持するために MutableState<T>
を使用する必要はありません。Jetpack Compose は他のオブザーバブルな型をサポートします。Jetpack Compose で別のオブザーバブルな型を読み取る前に、それを State<T>
に変換して、状態が変更されたときに Jetpack Compose が自動的に再コンポーズを実行できるようにする必要があります。
Compose には、Android アプリで使用される一般的なオブザーバブルな型から State<T>
を作成する関数が用意されています。
アプリがオブザーバブルなカスタムクラスを使用する場合は、拡張関数を作成して、Jetpack Compose が他のオブザーバブルな型を読み取れるようにすることができます。これを行う方法の例については、組み込み機能の実装を参照してください。Jetpack Compose がすべての変更をサブスクライブできるようにするオブジェクトは、すべて State<T>
に変換してコンポーザブルで読み取れるようにすることができます。
また、invalidate
を使用して手動で再コンポジションをトリガーすることにより、非オブザーバブルな状態オブジェクト用の統合レイヤを構築できます。この方法は、非オブザーバブルな型の相互運用が必要な状況でのみ使用する必要があります。invalidate
を使用したコードは誤りが生じやすく、オブザーバブルな状態オブジェクトを使用する同様のコードよりも読みづらい複雑なコードになりがちです。
内部状態を UI コンポーザブルから分離する
ExpandingCard
の最後のセクションには、内部状態があります。そのため、呼び出し元は状態を制御できません。つまり、展開された状態で ExpandingCard
を開始したい場合などに、それを行う手段がありません。別のイベント(ユーザーによる Fab
のクリックなど)に応じてカードを展開することもできません。また、expanded
状態を ViewModel
に移行したくても、そうすることはできません。
一方、状態の制御またはホイスティングを行う必要がない呼び出し元は、ExpandingCard
の内部状態を使用することにより、状態を自分で管理しなくてもそれを使用できます。
再利用可能なコンポーザブルを開発する際は、同じコンポーザブルのステートフル バージョンとステートレス バージョンの両方を公開することがよくあります。状態を考慮しない呼び出し元にとっては、ステートフル バージョンが便利です。状態の制御またはホイスティングを行う必要がある呼び出し元には、ステートレス バージョンが必要です。
ステートフル インターフェースとステートレス インターフェースの両方を提供するには、状態ホイスティングを使用して UI を表示するステートレスなコンポーザブルを抽出します。
2 つのコンポーザブルは、異なるパラメータを受け取るにもかかわらず、両方とも ExpandingCard
という名前になっていることにご注意ください。UI を出力するコンポーザブルの命名規則は、コンポーザブルが画面に表示するものを記述する CapitalCase 形式の名詞です。この場合は、どちらも ExpandingCard
を表示します。この命名規則は Compose ライブラリ全体に適用されます(例: TextField
、TextField
など)。
ExpandingCard
をステートフルなコンポーザブルとステートレスなコンポーザブルに分割する例を以下に示します。
// this stateful composable is only responsible for holding internal state
// and defers the UI to the stateless composable
@Composable
fun ExpandingCard(title: String, body: String) {
var expanded by remember { mutableStateOf(false) }
ExpandingCard(
title = title,
body = body,
expanded = expanded,
onExpand = { expanded = true },
onCollapse = { expanded = false }
)
}
// this stateless composable is responsible for describing the UI based on the state
// passed to it and firing events in response to the buttons being pressed
@Composable
fun ExpandingCard(
title: String,
body: String,
expanded: Boolean,
onExpand: () -> Unit,
onCollapse: () -> Unit
) {
Card {
Column(
Modifier
.width(280.dp)
.animateContentSize() // automatically animate size when it changes
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
) {
Text(title)
if (expanded) {
Spacer(Modifier.height(8.dp))
Text(body)
IconButton(onClick = onCollapse, Modifier.fillMaxWidth()) {
Icon(Icons.Default.ExpandLess)
}
} else {
IconButton(onClick = onExpand, Modifier.fillMaxWidth()) {
Icon(Icons.Default.ExpandMore)
}
}
}
}
}
Compose の状態ホイスティングは、状態をコンポーザブルの呼び出し元に移動してコンポーザブルをステートレスにするプログラミング パターンです。Jetpack Compose の状態ホイスティングの一般的なパターンでは、状態変数を次の 2 つのパラメータに置き換えます。
value: T
: 表示する現在の値。onValueChange: (T) -> Unit
: 値の変更をリクエストするイベント。T
は提案される新しい値です。
ただし、上記のパラメータは onValueChange
に限定されません。コンポーザブルに適した特定のイベントがある場合は、ExpandingCard
が onExpand
と onCollapse
を扱う場合と同様に、ラムダを使用してそのようなイベントを定義する必要があります。
この方法でホイストされる状態には、次のような重要な特性があります。
- 信頼できる唯一の情報源: 状態を複製するのではなく移動することで、
expanded
に関して信頼できる情報源を 1 つだけにすることができます。これは、バグを防ぐのに役立ちます。 - カプセル化: ステートフルな
ExpandingCard
だけがその状態を変更できます。これは完全に内部の状態です。 - 共有可能: ホイストされた状態を複数のコンポーザブルで共有できます。たとえば、
Card
が展開されているときにFab
ボタンを非表示にしたい場合、状態ホイスティングでそれを実現できます。 - インターセプト可能: ステートレスな
ExpandingCard
の呼び出し元は、状態を変更する前にイベントを無視するか変更するかを決定できます。 - 分離: ステートレスな
ExpandingCard
の状態はどこにでも保存できます。たとえば、title
、body
、expanded
をViewModel
に移動できます。
この方法を使用する場合も、単方向データフローに従います。状態はステートフルなコンポーザブルから下位に渡され、イベントはステートレスなコンポーザブルから上位に渡されます。

ExpandingCard
の単方向データフロー図内部状態と構成変更
Composition の remember
によって保存された値は、回転などの構成が変更された際に消去され、再作成されます。
remember { mutableStateOf(false) }
を使用している場合、ユーザーがスマートフォンを回転させるたびに、ステートフルな ExpandingCard
は折りたたまれた状態にリセットされます。これを解決するには、代わりに保存済みインスタンスの状態を使用して、構成変更時に状態を自動的に保存して復元します。
@Composable
fun ExpandingCard(title: String, body: String) {
var expanded by savedInstanceState { false }
ExpandingCard(
title = title,
body = body,
expanded = expanded,
onExpand = { expanded = true },
onCollapse = { expanded = false }
)
}
コンポーズ可能な関数 savedInstanceState<T>
は、構成変更時に自分自身を自動的に保存して復元する MutableState<T>
を返します。ユーザーが構成変更後も保持されることを期待する内部状態には、この関数を使用する必要があります。
詳細
状態と Jetpack Compose の詳細については、Jetpack Compose での状態の使用に関する Codelab をご覧ください。