アプリにおいて状態とは、時間とともに変化する可能性がある値を指します。これは非常に広範な定義であり、Room データベースにも、クラス内の変数一つにも当てはまります。
すべての Android アプリはユーザーに状態を表示します。Android アプリの状態の例を次にいくつか示します。
- ネットワーク接続を確立できないときに表示されるスナックバー。
- ブログ投稿と関連コメント。
- ユーザーがボタンをクリックしたときに再生されるボタンの波紋アニメーション。
- ユーザーが画像の上に描画できるステッカー。
Jetpack Compose では、Android アプリが状態をどこで、どのように保存し、使用するかの設定を明示的に行うことができます。このガイドでは、状態とコンポーザブルの関係に加え、Jetpack Compose が提供する、状態を簡単に処理するための API を中心に説明します。
状態とコンポジション
Compose は宣言型であるため、更新する唯一の方法は、新しい引数で同じコンポーザブルを呼び出すことです。この場合の引数は、UI の状態を表します。状態が更新されると、常に再コンポーズが行われます。そのため、命令型の XML ベースのビューとは異なり、TextField
などは自動更新されません。状態に応じた更新が行われるためには、コンポーザブルに新しい状態を明示する必要があります。
@Composable private fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
これを実行してテキストを入力しようとすると、何も起こりません。これは、TextField
が自身を更新しないためです。value
パラメータが変更されると、更新が発生します。これは、Compose のコンポジションと再コンポーズの仕組みによるものです。
初回コンポーズと再コンポーズの詳細については、Compose の思想をご覧ください。
コンポーザブル内の状態
コンポーズ可能な関数は、remember
API を使用してオブジェクトをメモリに格納できます。初回コンポーズの際に、remember
によって計算された値がコンポジションに保存され、保存された値は再コンポーズの際に返されます。remember
を使用すると、可変オブジェクトと不変オブジェクトの両方を保存できます。
mutableStateOf
はオブザーバブルな MutableState<T>
を作成します。これは、Compose ランタイムに統合されているオブザーバブルな型です。
interface MutableState<T> : State<T> {
override var value: T
}
value
を変更すると、value
を読み取るすべてのコンポーズ可能な関数の再コンポーズがスケジュール設定されます。
コンポーザブルの MutableState
オブジェクトを宣言するには、次の 3 つの方法があります。
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
これらの宣言は同等であり、状態のさまざまな用途に応じて糖衣構文として提供されます。作成するコンポーザブル向けに読みやすいコードを生成する構文を選択する必要があります。
by
デリゲート構文には、次のインポートが必要です。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
remember で保存される値を、他のコンポーザブルのパラメータとして使用できます。または、ステートメントのロジックとして使用して、表示されるコンポーザブルを変更することもできます。たとえば、名前が空の場合に挨拶を表示したくない場合は、以下のように if
ステートメントで状態を使用します。
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { var name by remember { mutableStateOf("") } if (name.isNotEmpty()) { Text( text = "Hello, $name!", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } ) } }
remember
を使用すると、状態は再コンポーズをまたいで保持されますが、設定の変更後は保持されません。保持するには、rememberSaveable
を使用する必要があります。rememberSaveable
は、Bundle
に保存可能なすべての値を自動的に保存します。その他の値については、カスタムのセーバー オブジェクトに渡すことができます。
その他のサポートされている状態の種類
Compose では、状態を保持するために MutableState<T>
を使用する必要はありません。他のオブザーバブルな型をサポートしています。Compose で別のオブザーバブルな型を読み取る前に、それを State<T>
に変換して、状態が変更されたときにコンポーザブルが自動的に再コンポーズを実行できるようにする必要があります。
Compose には、Android アプリで一般的に使用されるオブザーバブルな型から State<T>
を作成する関数が用意されています。これらの統合を使用する前に、下記の説明に沿って適切なアーティファクトを追加してください。
Flow
:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()
は、ライフサイクルを意識した方法でFlow
から値を収集します。そのため、アプリはアプリリソースを節約できます。これは、ComposeState
から出力された最新の値を表します。この API は、Android アプリでフローを収集するためのおすすめの方法として使用してください。build.gradle
ファイルには次の依存関係が必要です(2.6.0-beta01 以降であることが必要です)。
Kotlin
dependencies {
...
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}
Groovy
dependencies {
...
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
-
collectAsState
はcollectAsStateWithLifecycle
に似ています。こちらも、Flow
から値を収集して ComposeState
に変換します。プラットフォームに依存しないコードの場合は、Android のみの
collectAsStateWithLifecycle
ではなく、collectAsState
を使用します。compose-runtime
で使用できるため、collectAsState
には追加の依存関係は必要ありません。 -
observeAsState()
はこのLiveData
のモニタリングを開始し、State
を介してその値を表します。build.gradle
ファイルに、次のような依存関係が必要です。
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}
-
subscribeAsState()
は RxJava2 のリアクティブ ストリーム(例:Single
、Observable
、Completable
)を ComposeState
に変換する拡張関数です。build.gradle
ファイルに、次のような依存関係が必要です。
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}
-
subscribeAsState()
は RxJava3 のリアクティブ ストリーム(例:Single
、Observable
、Completable
)を ComposeState
に変換する拡張関数です。build.gradle
ファイルに、次のような依存関係が必要です。
Kotlin
dependencies {
...
implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}
Groovy
dependencies {
...
implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}
ステートフルとステートレス
remember
を使用してオブジェクトを保存するコンポーザブルは、内部状態を作成し、コンポーザブルをステートフルにします。たとえば、HelloContent
は、name
状態を内部に保持して変更するため、ステートフルなコンポーザブルです。これは呼び出し元が状態を制御する必要がない場合に便利です。状態を自分で管理しなくても使用できます。ただし、内部状態を持つコンポーザブルは、再利用性が低く、テストも難しくなりがちです。
ステートレスなコンポーザブルとは、一切の状態を保持しないコンポーザブルです。ステートレスは、状態ホイスティングを使用すると簡単に実現できます。
再利用可能なコンポーザブルを開発する際は、同じコンポーザブルのステートフル バージョンとステートレス バージョンの両方を公開することがよくあります。状態を考慮しない呼び出し元にとっては、ステートフル バージョンが便利です。状態の制御またはホイスティングを行う必要がある呼び出し元には、ステートレス バージョンが必要です。
状態ホイスティング
Compose の状態ホイスティングは、状態をコンポーザブルの呼び出し元に移動してコンポーザブルをステートレスにするプログラミング パターンです。Jetpack Compose の状態ホイスティングの一般的なパターンでは、状態変数を次の 2 つのパラメータに置き換えます。
value: T
: 表示する現在の値。onValueChange: (T) -> Unit
: 値の変更をリクエストするイベント。T
は提案される新しい値です。
ただし、上記のパラメータは onValueChange
に限定されません。コンポーザブルに適した特定のイベントがある場合は、ラムダを使用してそのようなイベントを定義する必要があります。
この方法でホイスティングされる状態には、次のような重要な特性があります。
- 信頼できる唯一の情報源: 状態を複製するのではなく移動することで、信頼できる情報源を 1 つだけにすることができます。これは、バグを防ぐのに役立ちます。
- カプセル化: 状態を変更できるのはステートフル コンポーザブルに限られます。完全に内部です。
- 共有可能: ホイスティングされた状態は複数のコンポーザブルで共有できます。別のコンポーザブルで
name
を読み取りたい場合は、ホイスティングでそれが可能になります。 - インターセプト可能: ステートレスなコンポーザブルの呼び出し元は、状態を変更する前にイベントを無視するか変更するかを決定できます。
- 分離: ステートレスなコンポーザブルの状態はどこにでも保存できます。たとえば、
name
をViewModel
に移動できます。
この例では、name
と onValueChange
を HelloContent
から抽出して、HelloContent
を呼び出すツリー上位の HelloScreen
コンポーザブルに移動します。
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { Text( text = "Hello, $name", modifier = Modifier.padding(bottom = 8.dp), style = MaterialTheme.typography.bodyMedium ) OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") }) } }
HelloContent
の状態をホイスティングすることで、コンポーザブルの使用を考慮し、さまざまな状況で再利用して、テストすることが容易になります。HelloContent
は、状態を保存する方法から切り離されています。これは、HelloScreen
を変更または置換する場合、HelloContent
の実装方法を変更する必要がないことを意味します。
状態が下降し、イベントが上昇するパターンは、単方向データフローと呼ばれます。この場合、状態は HelloScreen
から HelloContent
に下降し、イベントは HelloContent
から HelloScreen
に上昇します。単方向データフローに従うことで、UI に状態を表示するコンポーザブルと、状態を保存および変更するアプリの要素を切り離すことができます。
詳しくは、状態をホイスティングする場所のページをご覧ください。
Compose 内の状態を復元する
rememberSaveable
API は、再コンポーズ後も状態を保持し、保存されたインスタンス状態メカニズムを使用するアクティビティやプロセスの再作成後も状態を保持するため、remember
と同様に動作します。たとえば、画面が回転されたときに発生します。
状態を保存する方法
Bundle
に追加されたデータタイプはすべて、自動的に保存されます。Bundle
に追加できないものを保存する場合は、複数のオプションがあります。
Parcelize
最も簡単なのは、@Parcelize
アノテーションをオブジェクトに追加する方法です。オブジェクトが Parcelable になり、バンドルできます。たとえば、このコードは Parcelable な City
データ型を作成し、状態に保存します。
@Parcelize data class City(val name: String, val country: String) : Parcelable @Composable fun CityScreen() { var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) } }
mapSaver
なんらかの理由で @Parcelize
が適さない場合は、mapSaver
を使用して、オブジェクトを Bundle
に保存できる値のセットに変換するための独自ルールを定義できます。
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
listSaver
マップのキーを定義する必要がないようにするには、listSaver
を使用して、そのインデックスをキーとして使用することもできます。
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
Compose の状態ホルダー
シンプルな状態ホイスティングは、コンポーズ可能な関数自体で管理できます。ただし、トラッキングする状態の量が増加する場合や、コンポーズ可能な関数で実行するロジックが発生する場合は、ロジックと状態の役割を他のクラスの状態ホルダーにデリゲートすることをおすすめします。
Compose の状態ホイスティングのドキュメントをご覧ください。また、より一般的な内容については、アーキテクチャ ガイドの状態ホルダーと UI 状態のページをご覧ください。
キーが変更されたときに remember による計算を再トリガーする
remember
API は、多くの場合 MutableState
と併用されます。
var name by remember { mutableStateOf("") }
ここで、remember
関数を使用すると、MutableState
値が再コンポーズ後も保持されます。
一般的に、remember
は calculation
ラムダ パラメータを受け取ります。remember
が初めて実行されると、calculation
ラムダが呼び出され、結果が保存されます。再コンポーズの際に、remember
は最後に保存された値を返します。
キャッシュ状態とは別に、remember
を使用して、初期化や計算のコストが高いオブジェクトまたはオペレーションの結果を Composition に保存することもできます。この計算を再コンポーズのたびに繰り返すことはおすすめしません。例としては、次の ShaderBrush
オブジェクトの作成が挙げられます。これは高コストのオペレーションです。
val brush = remember { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) }
remember
は、Composition から出るまで値を保存します。ただし、キャッシュに保存された値を無効にする方法があります。remember
API は、key
パラメータまたは keys
パラメータも受け取ります。いずれかのキーが変更されると、次に関数が再コンポーズされたときに、remember
はキャッシュを無効にして計算ラムダブロックを再度実行します。このメカニズムにより、Composition 内のオブジェクトの存続期間を制御できます。この計算は、remember で保存されている値がコンポジションから出るまでではなく、入力が変更されるまで有効です。
次の例は、このメカニズムの仕組みを示しています。
このスニペットでは、ShaderBrush
が作成され、Box
コンポーザブルの背景塗りとして使用されます。前述のように、再作成にコストがかかるため、remember
は ShaderBrush
インスタンスを保存します。remember
は、選択された背景画像である key1
パラメータとして avatarRes
を受け取ります。avatarRes
が変化すると、ブラシは新しい画像で再コンポーズし、Box
に再適用されます。この処理は、ユーザーが選択ツールから背景に別の画像を選択したときに行われる可能性があります。
@Composable private fun BackgroundBanner( @DrawableRes avatarRes: Int, modifier: Modifier = Modifier, res: Resources = LocalContext.current.resources ) { val brush = remember(key1 = avatarRes) { ShaderBrush( BitmapShader( ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(), Shader.TileMode.REPEAT, Shader.TileMode.REPEAT ) ) } Box( modifier = modifier.background(brush) ) { /* ... */ } }
次のスニペットでは、状態はプレーンな状態ホルダークラス MyAppState
にホイスティングされます。この関数は rememberMyAppState
関数を公開し、remember
を使用してクラスのインスタンスを初期化します。このような関数を公開して再コンポーズ後も存続するインスタンスを作成することは、Compose では一般的なパターンです。rememberMyAppState
関数は windowSizeClass
を受け取ります。これは remember
の key
パラメータとして機能します。このパラメータが変更された場合、アプリはプレーンな状態ホルダークラスを最新の値で再作成する必要があります。これは、ユーザーがデバイスを回転させた場合などに発生することがあります。
@Composable private fun rememberMyAppState( windowSizeClass: WindowSizeClass ): MyAppState { return remember(windowSizeClass) { MyAppState(windowSizeClass) } } @Stable class MyAppState( private val windowSizeClass: WindowSizeClass ) { /* ... */ }
Compose は、クラスの equals 実装を使用して、キーが変更されたかどうかを判断し、保存されている値を無効にします。
再コンポーズ以外のキーを使用して状態を保存する
rememberSaveable
API は、データを Bundle
に保存できる remember
のラッパーです。この API を使用すると、状態は再コンポーズだけでなく、アクティビティの再作成やシステムによって開始されたプロセスの終了後も保持されます。rememberSaveable
は、remember
が keys
を受け取るのと同じ目的で、input
パラメータを受け取ります。入力のいずれかが変更されると、キャッシュは無効になります。次回関数が再コンポーズされたときに、rememberSaveable
が計算ラムダブロックを再実行します。
次の例では、typedQuery
が変更されるまで rememberSaveable
が userTypedQuery
を保存します。
var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) { mutableStateOf( TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length)) ) }
詳細
状態と Jetpack Compose の詳細については、以下の参考情報をご覧ください。
サンプル
Codelabs
動画
ブログ
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- Compose UI を設計する
- Compose で UI 状態を保存する
- Compose における副作用