Compose では UI は不変です。UI を描画した後で更新する手段はありません。制御できるのは UI の状態のみです。UI の状態が変化するたびに、Compose は UI ツリーの変化した部分を再作成します。コンポーザブルは、状態を受け取ってイベントを公開できます。たとえば、TextField
は値を受け取って、コールバック ハンドラに値の変更をリクエストするコールバック onValueChange
を公開します。
var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } )
コンポーザブルは状態を受け取ってイベントを公開するので、単方向データフロー パターンは Jetpack Compose に適しています。このガイドでは、Compose で単方向データフロー パターンを実装する方法、イベントと状態ホルダーを実装する方法、Compose で ViewModel を操作する方法に焦点を当てます。
単方向データフロー
単方向データフロー(UDF)は、状態が下方に流れ、イベントが上方に流れる設計パターンです。単方向データフローに従うことで、UI に状態を表示するコンポーザブルを、アプリ内で状態を保存および変更する部分から分離できます。
単方向データフローを使用するアプリの UI 更新ループは、次のようになります。
- イベント: UI の一部がイベントを生成して上方に渡します(たとえば、ボタンクリックが ViewModel に渡されて処理される場合)。または、アプリの他のレイヤからイベントが渡されます(たとえば、ユーザー セッションの有効期限が切れていることを示す場合)。
- 状態の更新: イベント ハンドラが状態を変更します。
- 状態の表示: 状態ホルダーが状態を下方に渡し、UI が状態を表示します。
Jetpack Compose を使用する際にこのパターンに従うと、次のような利点があります。
- テストの容易性: 状態を表示する UI から状態を分離することで、両者を切り離して簡単にテストできます。
- 状態のカプセル化: 状態が 1 か所でのみ更新され、コンポーザブルの状態に関して信頼できる情報源が 1 つだけになるため、状態の不整合によるバグが生じる可能性が低くなります。
- UI の整合性:
StateFlow
やLiveData
などのオブザーバブルな状態ホルダーを使用すると、すべての状態の更新が UI に即座に反映されます。
Jetpack Compose の単方向データフロー
コンポーザブルは、状態とイベントに基づいて動作します。たとえば、TextField
は、その value
パラメータが更新され、onValueChange
コールバックを公開する(新しい値への変更をリクエストするイベントが発生する)場合にのみ、更新されます。Compose は State
オブジェクトを値ホルダーとして定義し、状態の値が変化すると再コンポジションがトリガーされます。値を記憶する期間に応じて、remember { mutableStateOf(value) }
または rememberSaveable { mutableStateOf(value)
で状態を保持できます。
TextField
コンポーザブルの値の型は String
であるため、どこからでも取得できます。たとえば、ハードコードされた値、ViewModel、または親コンポーザブルから渡すことができます。State
オブジェクト内に値を保持する必要はありませんが、onValueChange
が呼び出されたときは値を更新する必要があります。
コンポーザブルのパラメータを定義する
コンポーザブルの状態パラメータを定義する際は、次の点に注意してください。
- コンポーザブルはどの程度再利用可能か(柔軟性があるか)
- 状態パラメータがコンポーザブルのパフォーマンスにどのように影響するか
分離と再利用を促進するため、個々のコンポーザブルで保持する情報はできる限り少なくする必要があります。たとえば、ニュース記事のヘッダーを保持するコンポーザブルを作成する場合は、ニュース記事全体ではなく、表示する必要がある情報のみを渡します。
@Composable fun Header(title: String, subtitle: String) { // Recomposes when title or subtitle have changed. } @Composable fun Header(news: News) { // Recomposes when a new instance of News is passed in. }
個別のパラメータを使用すると、パフォーマンスが向上することもあります。たとえば、News
に title
と subtitle
以上の情報が含まれている場合、News
の新しいインスタンスが Header(news)
に渡されるたびに、title
と subtitle
が変更されていなくてもコンポーザブルは再コンポーズされます。
受け渡すパラメータの数は慎重に検討してください。関数に渡すパラメータが多すぎると関数のエルゴノミクスが低下するため、その場合はパラメータを 1 つのクラスにまとめることをおすすめします。
Compose 内のイベント
アプリへのすべての入力は、イベント(タップやテキストの変更。タイマーなどの更新も含む)として表す必要があります。これらのイベントにより UI の状態が変わるため
ViewModel
は、それらのイベントを処理し、UI 状態を更新するものであるべきです。
UI レイヤは、イベント ハンドラの外部で状態を変更するべきではありません。アプリに不整合とバグが生じる可能性があるからです。
状態とイベント ハンドラのラムダに不変の値を渡すことを優先してください。この方法には次のメリットがあります。
- 再利用性が向上します。
- UI が状態の値を直接変更しないことが保証されます。
- 状態が別のスレッドから変更されないことが保証されるため、同時実行の問題を回避できます。
- 多くの場合、コードの複雑さが軽減されます。
たとえば、String
とラムダをパラメータとして受け入れるコンポーザブルは、多様なコンテキストから呼び出すことができるため、再利用性が高くなります。アプリの上部アプリバーに、テキストが常に表示され、[戻る] ボタンがあるとします。この場合、テキストと [戻る] ボタンのハンドルをパラメータとして受け取る汎用的な MyAppTopAppBar
コンポーザブルを定義できます。
@Composable fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) { TopAppBar( title = { Text( text = topAppBarText, textAlign = TextAlign.Center, modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) }, navigationIcon = { IconButton(onClick = onBackPressed) { Icon( Icons.Filled.ArrowBack, contentDescription = localizedString ) } }, // ... ) }
ViewModel、状態、イベントの例
ViewModel
と mutableStateOf
を使用して、次のいずれかに該当する場合に、単方向データフローをアプリに導入することもできます。
- UI の状態が、
StateFlow
、LiveData
など、オブザーバルな状態ホルダーを介して公開される。 ViewModel
が UI またはアプリの他のレイヤからのイベントを処理し、イベントに基づいて状態ホルダーを更新する。
たとえば、ログイン画面を実装する場合、ログインボタンがタップされると、アプリは進行状況スピナーとネットワーク呼び出しを表示する必要があります。ログインが成功した場合、アプリは別の画面にナビゲートします。エラーが発生した場合は、スナックバーを表示します。画面の状態とイベントをモデル化する方法を次に示します。
画面には次の 4 つの状態があります。
- ログアウト: ユーザーがまだログインしていない状態。
- 進行中: アプリがネットワーク呼び出しを実行して、ユーザーのログインを試行している状態。
- エラー: ログイン中にエラーが発生した状態。
- ログイン: ユーザーがログインしている状態。
これらの状態は、シールクラスとしてモデル化できます。ViewModel
は状態を次のように公開します。
State
を呼び出して初期状態を設定し、必要に応じて状態を更新します。「
ViewModel
は、onSignIn()
メソッドを公開することでログイン イベントも処理します。
class MyViewModel : ViewModel() { private val _uiState = mutableStateOf<UiState>(UiState.SignedOut) val uiState: State<UiState> get() = _uiState // ... }
Compose は、mutableStateOf
API に加えて、LiveData
、Flow
、Observable
の拡張機能を提供します。これらをリスナーとして登録し、値を状態として表します。
class MyViewModel : ViewModel() { private val _uiState = MutableLiveData<UiState>(UiState.SignedOut) val uiState: LiveData<UiState> get() = _uiState // ... } @Composable fun MyComposable(viewModel: MyViewModel) { val uiState = viewModel.uiState.observeAsState() // ... }
詳細
Jetpack Compose のアーキテクチャについて詳しくは、以下のリソースをご覧ください。
サンプル
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- 状態と Jetpack Compose
- Compose で UI の状態を保存する
- ユーザー入力を処理する