1. 始める前に
この Codelab では、Jetpack Compose の状態の使用に関する主要コンセプトについて説明します。アプリの状態から UI に表示されるものを特定する方法、複数の API を使用して状態が変わったときに Compose が UI を更新する方法、コンポーズ可能な関数の構造を最適化する方法、Compose の世界で ViewModel を使用する方法について説明します。
前提条件
- Kotlin 構文に関する知識。
- Compose に関する基礎知識(Jetpack Compose チュートリアルから始められます)。
- アーキテクチャ コンポーネントの
ViewModel
に関する基礎知識。
学習内容
- Jetpack Compose UI での状態とイベントの考え方。
- Compose が状態を使用して画面に表示する要素を決定する方法。
- 状態ホイスティングの概要。
- ステートフルおよびステートレスなコンポーズ可能な関数の仕組み。
- Compose が
State<T>
API を使用して状態を自動的に追跡する方法。 - コンポーズ可能な関数におけるメモリと内部状態の仕組み(
remember
API とrememberSaveable
API の使用)。 - リストと状態を処理する方法(
mutableStateListOf
API とtoMutableStateList
API の使用)。 - Compose で
ViewModel
を使用する方法。
必要なもの
推奨 / 任意
- Compose の思想を読む。
- この Codelab の前に Jetpack Compose の基本の Codelab をご覧ください。この Codelab では、状態についてのまとめを行います。
作成するアプリの概要
簡単なウェルネス アプリを実装します。
このアプリには主に 2 つの機能があります。
- 水分摂取を追跡するためのウォーター カウンタ。
- 一日を通して行うウェルネス タスクのリスト。
この Codelab の学習を進める際のサポートとして、次の Code-Along 動画をご覧ください。
2. 設定する
新しい Compose プロジェクトを開始する
- 新しい Compose プロジェクトを開始するには、Android Studio を開きます。
- [Welcome to Android Studio] ウィンドウが開いている場合は、[Start a new Android Studio] プロジェクトをクリックします。Android Studio プロジェクトをすでに開いている場合は、メニューバーで [File] > [New] > [New Project] を選択します。
- 新しいプロジェクトの場合は、利用可能なテンプレートの中から [Empty Activity] を選択します。
- [Next] をクリックし、プロジェクトを設定して、「BasicStateCodelab」という名前を付けます。
API レベル 21(API Compose がサポートする最小レベル)以上の minimumSdkVersion を選択してください。
[Empty Compose Activity] テンプレートを選択すると、Android Studio によって、プロジェクトに以下のものが設定されます。
- 画面上にテキストを表示するコンポーズ可能な関数で構成された
MainActivity
クラス。 AndroidManifest.xml
ファイル。アプリの権限、コンポーネント、カスタム リソースを定義します。build.gradle.kts
ファイルとapp/build.gradle.kts
ファイルには、Compose に必要なオプションと依存関係が含まれています。
Codelab の解答
BasicStateCodelab
の解答コードは、次のコマンドを実行して GitHub から入手できます。
$ git clone https://github.com/android/codelab-android-compose
または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。
解答コードは BasicStateCodelab
プロジェクトにあります。自分のペースで順を追って Codelab の学習を進め、必要があれば解答を確認することをおすすめします。Codelab の途中には、プロジェクトに追加する必要があるコード スニペットを記載しています。
3.Compose の状態
アプリにおいて「状態」とは、時間とともに変化する可能性がある値すべてを指します。これは非常に広範な定義であり、Room データベースにも、クラス内の変数一つにも当てはまります。
すべての Android アプリはユーザーに状態を表示します。Android アプリの状態の例を次にいくつか示します。
- チャットアプリで受信した最新のメッセージ。
- ユーザーのプロフィール写真。
- アイテムのリスト内のスクロール位置。
ウェルネス アプリの作成を始めましょう。
わかりやすくするため、この Codelab では次のようにしています。
- すべての Kotlin ファイルを
app
モジュールのルートcom.codelabs.basicstatecodelab
パッケージに追加できます。ただし、本番環境のアプリでは、ファイルはサブパッケージで論理的に構造化する必要があります。 - すべての文字列をスニペットにインラインでハードコードします。実際のアプリでは、
strings.xml
ファイルに文字列リソースとして追加し、Compose のstringResource
API を使用して参照する必要があります。
作成が必要な最初の機能は、1 日の水の摂取量をグラス単位でカウントするウォーター カウンタです。
グラスの数を表示する Text
コンポーザブルが含まれる WaterCounter
というコンポーズ可能な関数を作成します。グラスの数は count
という値に保存する必要があります。ここではこの値をハードコードできます。
次のように、コンポーズ可能な関数 WaterCounter
を使用して、新しいファイル WaterCounter.kt
を作成します。
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses.",
modifier = modifier.padding(16.dp)
)
}
次に、画面全体を表すコンポーズ可能な関数を作成します。この関数は、2 つのセクション(ウォーター カウンタと、ウェルネス タスクのリスト)で構成されます。ここでは、カウンタを追加します。
- メイン画面を表す
WellnessScreen.kt
ファイルを作成し、WaterCounter
関数を呼び出します。
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
MainActivity.kt
を開きます。Greeting
コンポーザブルとDefaultPreview
コンポーザブルを削除します。次のように、アクティビティのsetContent
ブロック内で、新しく作成したWellnessScreen
コンポーザブルを呼び出します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicStateCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WellnessScreen()
}
}
}
}
}
- アプリを実行すると、基本的なウォーター カウンタの画面が表示され、水の量がグラスの数としてハードコードされています。
コンポーズ可能な関数 WaterCounter
の状態は、変数 count
です。しかし、静的な状態は変更できないため、あまり便利ではありません。これを修正するには、Button
を追加してカウントを増やし、1 日あたりの水の摂取量をグラス数として追跡します。
状態を変更するアクションはすべて「イベント」と呼ばれます。これについては、次のセクションで詳しく説明します。
4. Compose 内のイベント
状態とは、時間の経過とともに変化する値(チャットアプリで最後に受信したメッセージなど)を指します。では、状態が更新される原因は何でしょうか。Android アプリでは、イベントに応答して状態が更新されます。
イベントは、アプリケーションの内外で生成される入力です。以下に例を示します。
- ユーザーがボタンを押すなどの操作で UI を操作する。
- その他の要因(センサーによる新しい値の送信、ネットワーク レスポンスなど)。
アプリの状態は UI に表示する内容を示しますが、イベントは状態を変化させるメカニズムであり、その結果 UI が変化します。
イベントは、何かが起こったことをプログラムの要素に通知します。すべての Android アプリは、次のようなコア UI 更新ループを備えています。
- イベント – ユーザーまたはプログラムの要素によってイベントが生成されます。
- 状態の更新 – UI で使用される状態がイベント ハンドラにより変更されます。
- 状態の表示 – UI が更新され、新しい状態を表示します。
状態とイベントの相互作用を理解すれば、Compose の状態を扱えるようになります。
では、このボタンを追加して、ユーザーがグラスの水を追加して状態を変更できるようにします。
コンポーズ可能な関数 WaterCounter
に移動し、ラベル Text
の下に Button
を追加します。Column
を使用すると、Text
を Button
コンポーザブルに上下に配置できます。外部パディングを Column
コンポーザブルに移動し、Button
の上部にパディングを追加して、テキストから分離します。
コンポーズ可能な関数 Button
は、ラムダ関数 onClick
を受け取ります。これは、ボタンがクリックされたときに発生するイベントです。ラムダ関数の例は後で詳しく説明します。
count
を val
から var
に変更して、可変にします。
import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
アプリを実行してボタンをクリックしても、何も起こりません。count
変数に異なる値が設定されても、Compose はそれを「状態変更」として検出しないため、何も起こりません。これは、状態が変更されたときに画面を再描画(つまり、コンポーズ可能な関数を「再コンポーズ」)する必要があることを Compose に指示していないためです。これを次のステップで修正します。
5. コンポーズ可能な関数のメモリ
Compose アプリは、コンポーズ可能な関数を呼び出すことにより、データを UI に変換します。ここでは、Compose がコンポーザブルの実行時に構築する UI の記述を「コンポジション」と呼びます。状態が変更されると、Compose は影響を受けるコンポーズ可能な関数を新しい状態で再実行し、更新された UI を作成します。これを再コンポーズと呼びます。また、Compose は個々のコンポーザブルが必要とするデータを確認するため、データが変更されたコンポーネントのみを再コンポーズし、影響を受けないコンポーネントはスキップします。
これを行うには、Compose はトラッキングする状態を認識している必要があります。これにより、更新を受信したときに再コンポーズをスケジュール設定できるようになります。
Compose には、特定の状態を読み取るすべてのコンポーザブルの再コンポーズをスケジュール設定する、特別な状態トラッキング システムが用意されています。これにより Compose で細かい作業が可能になり、UI 全体ではなく、変更が必要となるコンポーズ可能な関数を再コンポーズするだけで済みます。これを行うには、状態の書き込み(「状態の変化」)だけでなく、状態の「読み取り」も追跡します。
Compose の State
型と MutableState
型を使用して、Compose が状態を監視できるようにします。
Compose は、状態 value
プロパティを読み取り、その value
が変更されたときに再コンポーズをトリガーする各コンポーザブルをトラッキングします。mutableStateOf
関数を使用して、監視可能な MutableState
を作成できます。初期値をパラメータとして受け取って State
オブジェクトにラップすることで、value
を監視できるようになります。
WaterCounter
コンポーザブルを更新し、count
が 0
を初期値として mutableStateOf
API を使用するようにします。mutableStateOf
が MutableState
型を返すため、value
を更新して状態を更新すると、value
を読み取る関数への再コンポーズが Compose でトリガーされます。
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
// Changes to count are now tracked by Compose
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
前述のように、count
を変更すると、count
の value
を自動的に読み取るコンポーズ可能な関数の再コンポーズがスケジュール設定されます。この場合、ボタンがクリックされるたびに WaterCounter
が再コンポーズされます。
いまアプリを実行しても、やはりまだ何も起こりません。
再コンポーズのスケジュールは正常に機能しています。ただし、再コンポーズの際には、変数 count
が 0 に再初期化されるため、この値を再コンポーズ後も保持する方法が必要です。
このためには、コンポーズ可能なインライン関数 remember
を使用できます。remember
によって計算された値は、初回コンポーズ中にコンポジションに保存され、保存された値は再コンポーズ後も保持されます。
通常、remember
と mutableStateOf
はコンポーズ可能な関数で一緒に使用されます。
Compose の状態に関するドキュメントで説明しているように、これに相当する記述方法があります。
WaterCounter
を変更して、mutableStateOf
の呼び出しをインラインのコンポーズ可能な関数 remember
で囲みます。
import androidx.compose.runtime.remember
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
別の方法として、Kotlin の委譲プロパティを使用することで count
の使用を簡素化することもできます。
count
を変数として定義するには、by キーワードを使用します。委任のゲッターとセッターのインポートを追加すると、毎回 MutableState
の value
プロパティを明示的に参照せずに、間接的に count
を読み取り、変更できます。
WaterCounter
は次のようになります。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
作成するコンポーザブル向けに読みやすいコードを生成する構文を選択する必要があります。
ここまでの内容を確認しましょう。
- 時間の経過とともに記憶する
count
という変数を定義しました。 - 記憶された数字をユーザーに知らせるテキスト表示を作成しました。
- クリックするたびに記憶された数字を増やすボタンを追加しました。
この構成は、ユーザーとのデータフロー フィードバック ループを形成します。
- UI でユーザーに状態が表示されます(現在のカウントがテキストとして表示されます)。
- ユーザーがイベントを生成し、これが既存の状態と組み合わされて新しい状態を生成します(ボタンをクリックすると、現在の数に 1 つ追加されます)。
これでカウンタが動作します。
6. 状態駆動型 UI
Compose は宣言型 UI フレームワークです。UI コンポーネントの削除や、状態変化時の表示 / 非表示の変更の代わりに、UI が特定の状態条件下でどのように表示されるかを説明します。再コンポーズが呼び出されて UI が更新された場合、コンポーザブルは最終的にコンポジションに入る、またはコンポジションから出ていく可能性があります。
このアプローチでは、ビューシステムのように手動でビューを更新する複雑な作業を回避できます。また、自動で行われるため、新しい状態に基づいて必ずビューが更新されるため、エラーが発生しにくくなります。
コンポーズ可能な関数が初回コンポーズまたは再コンポーズで呼び出される場合、その関数はコンポジション内に存在します。呼び出されないコンポーズ可能な関数(その関数が if ステートメントで呼び出されているが、条件が満たされていない場合など)は、コンポジション内に存在しません。
コンポーザブルのライフサイクルの詳細については、ドキュメントをご覧ください。
コンポジションの出力は、UI を記述したツリー構造になります。
Compose によって生成されたアプリ レイアウトは、Android Studio の Layout Inspector ツールを使用して検査できます。この作業はこの次に行います。
このツールを説明するために、状態に基づいて UI を表示するようにコードを変更します。WaterCounter
を開き、count
が 0 より大きい場合に Text
を表示します。
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
アプリを実行し、[Tools] > [Layout Inspector] に移動して、Android Studio の Layout Inspector ツールを開きます。
分割画面が表示されます。左側にはコンポーネント ツリー、右側にはアプリのプレビューが表示されます。
画面の左側にあるルート要素 BasicStateCodelabTheme
をタップして、ツリー内を移動します。[Expand all] ボタンをクリックして、コンポーネント ツリー全体を開きます。
右側の画面の要素をクリックすると、ツリーの対応する要素に移動します。
アプリで [Add one] ボタンを押すと、次のようになります。
- カウントが 1 に増え、状態が変化します。
- 再コンポーズが呼び出されます。
- 画面が新しい要素で再コンポーズされます。
Android Studio の Layout Inspector ツールでコンポーネント ツリーを調べると、Text
コンポーザブルも表示されるようになっています。
状態により、特定の時点でどの要素が UI に表示されるかが決まります。
UI の複数の部分が同じ状態に依存することがあります。Button
を変更して、count
が 10 になるまで有効となり、その後無効になるようにします(これで今日の目標は達成されます)。これを行うには、Button
の enabled
パラメータを使用します。
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
...
}
今すぐアプリを実行します。状態 count
の変更によって、Text
を表示するかどうか、および Button
が有効か無効かが決まります。
7. コンポジションに保存する
remember
はコンポジションにオブジェクトを保存し、再コンポーズの際に remember
が呼び出されるソース位置が再度呼び出されないと、そのオブジェクトを消去します。
この動作を可視化するには、アプリに次の機能を実装します。ユーザーが水を 1 杯以上飲んでいる場合は、ユーザーが行うウェルネス タスクを表示して、閉じることもできるようにします。コンポーザブルは小さくて再利用可能であるため、WellnessTaskItem
という新しいコンポーザブルを作成します。これは、パラメータとして受け取った文字列に基づいてウェルネス タスクを表示し、さらに [Close] アイコンボタンを表示します。
新しいファイル WellnessTaskItem.kt
を作成し、次のコードを追加します。このコンポーズ可能な関数は、この Codelab で後ほど使用します。
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding
@Composable
fun WellnessTaskItem(
taskName: String,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(start = 16.dp),
text = taskName
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
WellnessTaskItem
関数は、タスクの説明と onClose
ラムダ関数を受け取ります(組み込みの Button
コンポーザブルが onClick
を受け取るのと同様)。
WellnessTaskItem
は次のようになります。
機能を追加してアプリを改善するには、WaterCounter
を更新して、count
> 0 のとき WellnessTaskItem
が表示されるようにします。
count
が 0 より大きい場合、WellnessTaskItem
を表示するかどうかを決定する変数 showTask
を定義し、それを true に初期化します。
新しい if ステートメントを追加して、showTask
が true の場合に WellnessTaskItem
を表示します。前のセクションで学習した API を使用して、showTask
値が再コンポーズ後も保持されるようにします。
@Composable
fun WaterCounter() {
Column(modifier = Modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
WellnessTaskItem
の onClose
ラムダ関数を使用して、X ボタンが押されると、変数 showTask
が false
に変更され、タスクが表示されなくなるようにします。
...
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
...
次に、「Clear water count」というテキストがある新しい Button
を追加し、「Add one」Button
の横に配置します。Row
は 2 つのボタンの配置に役立ちます。Row
にパディングを追加することもできます。[Clear water count] ボタンが押されると、変数 count
は 0 にリセットされます。
完成したコンポーズ可能な関数 WaterCounter
は次のようになります。
import androidx.compose.foundation.layout.Row
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(
onClick = { count = 0 },
Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
アプリを実行すると、画面に初期状態が表示されます。
右側には、簡素化されたコンポーネント ツリーが表示されています。これにより、状態の変化に伴う状況を分析できます。count
と showTask
は保存されている値です。
アプリで以下の手順を行います。
- [Add one] ボタンを押します。これにより、
count
の値が増え(再コンポジションが発生します)、WellnessTaskItem
とカウンタText
の両方が表示されます。
WellnessTaskItem
コンポーネントの X を押します(別の再コンポーズが発生します)。showTask
が false になったため、WellnessTaskItem
が表示されなくなります。
- [Add one] ボタンを押します(別の再コンポーズが発生します)。グラスを追加し続けると、
showTask
は次の再コンポジションでWellnessTaskItem
を閉じたことを記憶します。
- [Clear water count] ボタンを押して
count
を 0 にリセットし、再コンポーズを行います。count
を表示するText
と、WellnessTaskItem
に関連するすべてのコードは呼び出されず、コンポジションから出ます。
- 保存された
showTask
が呼び出されるコード位置が呼び出されなかったため、showTask
は破棄されました。最初の手順に戻ります。
- [Add one] ボタンを押して、
count
を 0 より大きくします(再コンポーズ)。
WellnessTaskItem
コンポーザブルは、上のコンポジションから離れたときに以前のshowTask
値が破棄されたため、再度表示されます。
count
が 0 に戻った後で、remember
で許容される値より長く showTask
が持続する必要がある場合(つまり、remember
が呼び出されるコード位置が再コンポーズの際に呼び出されなかった場合)、どうなるでしょうか。以降のセクションでは、これらのシナリオの修正方法とその他の例について説明します。
コンポジションから移動したときに UI と状態がどのようにリセットされるかを理解しました。次に、コードをクリアして、このセクションの最初で説明した WaterCounter
に戻ります。
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
8. Compose の状態を復元する
アプリを実行し、グラス何杯かの水をカウンタに追加して、デバイスを回転させます。デバイスの自動回転の設定がオンになっていることを確認してください。
アクティビティは設定(この場合は画面の向き)の変更後に再作成されるため、保存された状態は破棄され、カウンタは 0 に戻ると消えます。
言語を変更したり、ダークモードとライトモードを切り替えたりなど、Android で実行中のアクティビティを再作成するようなその他の設定変更を行った場合でも同様です。
remember
を使用すると、状態は再コンポーズ後も保持されますが、設定の変更後は保持されません。そのためには、remember
ではなく rememberSaveable
を使用する必要があります。
rememberSaveable
は、Bundle
に保存可能なすべての値を自動的に保存します。その他の値については、カスタムのセーバー オブジェクトに渡すことができます。Compose での状態の復元について詳しくは、ドキュメントをご覧ください。
WaterCounter
で、remember
を rememberSaveable
に置き換えます。
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
アプリを実行して、設定を変更してみます。カウンタが適切に保存されていることを確認します。
アクティビティの再作成は、rememberSaveable
のユースケースの一つにすぎません。別のユースケースでは、リストを使用して作業します。
アプリの状態と UX のニーズに応じて remember
と rememberSaveable
のどちらを使用するかを検討してください。
9. 状態ホイスティング
remember
を使用してオブジェクトを保存するコンポーザブルには内部状態が含まれ、コンポーザブルをステートフルにします。これは呼び出し元が状態を制御する必要がない場合に便利です。状態を自分で管理しなくても使用できます。ただし、内部状態を持つコンポーザブルは、再利用性が低く、テストも難しくなりがちです。
状態を保持しないコンポーザブルは、ステートレスなコンポーザブルと呼ばれます。ステートレス コンポーザブルは、状態ホイスティングを使用すると簡単に作成できます。
Compose の状態ホイスティングは、状態をコンポーザブルの呼び出し元に移動してコンポーザブルをステートレスにするプログラミング パターンです。Jetpack Compose の状態ホイスティングの一般的なパターンでは、状態変数を次の 2 つのパラメータに置き換えます。
- value: T - 表示する現在の値
- onValueChange: (T) -> Unit - 新しい値 T で値の変更をリクエストするイベント
ここで、この値は変更可能な状態を表します。
この方法でホイスティングされる状態には、次のような重要な特性があります。
- 信頼できる唯一の情報源: 状態を複製するのではなく移動することで、信頼できる情報源を 1 つだけにすることができます。これは、バグを防ぐのに役立ちます。
- 共有可能: ホイスティングされた状態は複数のコンポーザブルで共有できます。
- インターセプト可能: ステートレスなコンポーザブルの呼び出し元は、状態を変更する前にイベントを無視するか変更するかを決定できます。
- 分離: ステートレスでコンポーズ可能な関数の状態は、どこにでも保存できます。たとえば、ViewModel でも可能です。
これらすべてのメリットが得られるように、WaterCounter
で実装するようにしてください。
ステートフルとステートレス
コンポーズ可能な関数からすべての状態を抽出できる場合、生成されるコンポーズ可能な関数はステートレスと呼ばれます。
WaterCounter
コンポーザブルを、ステートフル カウンタとステートレス カウンタの 2 つの部分に分割してリファクタリングします。
StatelessCounter
の役割は、count
を表示し、count
の値を増やすときに関数を呼び出すことです。そのためには、上記のパターンに従い、状態 count
(コンポーズ可能な関数のパラメータとして)と、状態を増加させる必要があるときに呼び出されるラムダ(onIncrement
)を渡します。StatelessCounter
は次のようになります。
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
StatefulCounter
は状態を保有します。つまり、count
状態を保持し、StatelessCounter
関数を呼び出すとこれを変更します。
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
よくできました!StatelessCounter
から StatefulCounter
にcount
をホイスティングしました。
これをアプリにプラグインし、WellnessScreen
を StatefulCounter
で更新できます。
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
前述のように、状態ホイスティングにはいくつかのメリットがあります。ここでは、コードの一部を説明していきます。以下のスニペットをアプリにコピーする必要はありません。
- ステートレスなコンポーザブルを再利用できるようになりました。次に例を示します。
水とジュースの量をグラス単位でカウントするには、waterCount
と juiceCount
を保存しますが、同一のコンポーズ可能な関数 StatelessCounter
を使用すると、2 つの異なる独立した状態を表示できます。
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
juiceCount
が変更されると、StatefulCounter
が再コンポーズされます。再コンポーズの際に、Compose は juiceCount
を読み取る関数を特定し、それらの関数の再コンポーズのみをトリガーします。
ユーザーがタップして juiceCount
の値を増加させると、StatefulCounter
が再コンポーズされ、juiceCount
を読み取る StatelessCounter
も再コンポーズされます。ただし、waterCount
を読み取る StatelessCounter
は再コンポーズされません。
- ステートフルでコンポーズ可能な関数は、複数のコンポーズ可能な関数に同じ状態を提供できます。
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
この場合、カウントが StatelessCounter
または AnotherStatelessMethod
で更新されると、すべてが再コンポーズされます。これは想定された結果です。
ホイスティングされた状態は共有可能なため、不要な再コンポーズを回避し、再利用性を高めるために、コンポーザブルに必要な状態のみを渡すようにしてください。
状態と状態ホイスティングについて詳しくは、Compose の状態に関するドキュメントをご覧ください。
10. リストを操作する
次に、アプリの 2 番目の機能であるウェルネス タスクのリストを追加します。リスト内のアイテムに対して実行できる操作は 2 つあります。
- リストアイテムをチェックして、タスクを完了としてマークする。
- 完了させるつもりがないタスクをリストから削除する。
セットアップ
- まず、リストアイテムを変更します。「コンポジションに保存する」の項の
WellnessTaskItem
を再利用し、Checkbox
が含まれるように更新できます。関数をステートレスにするために、checked
状態とonCheckedChange
コールバックをホイスティングしてください。
このセクションの WellnessTaskItem
コンポーザブルは次のようになります。
import androidx.compose.material3.Checkbox
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- 同じファイルで、ステートフルなコンポーズ可能な関数
WellnessTaskItem
を追加します。この関数は状態変数checkedState
を定義して、同じ名前のステートレス メソッドに渡します。onClose
については、今回は空のラムダ関数を渡しても問題ありません。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
WellnessTask.kt
ファイルを作成して、ID とラベルを含むタスクをモデル化します。これをデータクラスとして定義します。
data class WellnessTask(val id: Int, val label: String)
- タスクのリスト自体には、
WellnessTasksList.kt
という名前の新しいファイルを作成し、架空のデータを生成するメソッドを追加します。
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
実際のアプリでは、データレイヤーからデータが取得されます。
WellnessTasksList.kt
で、リストを作成するコンポーズ可能な関数を追加します。LazyColumn
と、作成したリストメソッドのアイテムを定義します。サポートが必要な場合は、リストのドキュメントをご覧ください。
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
@Composable
fun WellnessTasksList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTasks() }
) {
LazyColumn(
modifier = modifier
) {
items(list) { task ->
WellnessTaskItem(taskName = task.label)
}
}
}
- リストを
WellnessScreen
に追加します。Column
を使用すると、リストと既存のカウンタを縦方向に揃えることができます。
import androidx.compose.foundation.layout.Column
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
- アプリを実行して、試してみましょう。タスクの確認はできるようになりましたが、削除はできません。これは後のセクションで実装します。
LazyList でアイテムの状態を復元する
ここでは、WellnessTaskItem
コンポーザブルの詳細について説明します。
checkedState
は、プライベート変数と同様に、各 WellnessTaskItem
コンポーザブルに独立して属します。checkedState
を変更すると、LazyColumn
内のすべての WellnessTaskItem
インスタンスではなく、WellnessTaskItem
のそのインスタンスのみが再コンポーズされます。
次の手順に沿って試してください。
- このリストの上部にある要素(要素 1、要素 2 など)をオンにします。
- リストの一番下までスクロールして、これらの要素が画面の外に出るようにします。
- 一番上までスクロールして、先ほどオンにした項目を表示します。
- オフになっていることを確認します。
前のセクションで確認したように、アイテムがコンポジションから出ると、保存された状態が破棄されるという問題があります。LazyColumn
のアイテムの場合、スクロールするとアイテムはコンポジションから完全に離れ、表示されなくなります。
どうすれば解決できるでしょうか。ここでも、rememberSaveable
を使用します。保存された値は、保存されたインスタンス状態メカニズムを使用するアクティビティまたはプロセスの再作成の後も存続します。rememberSaveable
が LazyList
と連携する仕組みにより、アイテムはコンポジションを離れても存続できます。
ステートフル WellnessTaskItem
で remember
を rememberSaveable
に置き換えるだけです。
import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }
Compose の一般的なパターン
LazyColumn
の実装に注意してください。
@Composable
fun LazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
コンポーズ可能な関数 rememberLazyListState
は、rememberSaveable
を使用してリストの初期状態を作成します。アクティビティが再作成されると、スクロール状態は維持されます。コーディングは必要ありません。
多くのアプリでは、スクロール位置、アイテム レイアウトの変更、リストの状態に関連するその他のイベントに反応してリッスンする必要があります。LazyColumn
や LazyRow
などの遅延コンポーネントは、LazyListState
をホイスティングすることでこのユースケースをサポートします。このパターンについて詳しくは、リストの状態に関するドキュメントをご覧ください。
パブリックな rememberX
関数によりデフォルト値の状態パラメータを使用することは、組み込みのコンポーズ可能な関数では一般的なパターンです。別の例は、BottomSheetScaffold
にあります。これは rememberBottomSheetScaffoldState
を使用して状態をホイスティングします。
11. オブザーバブルな MutableList
次に、リストからタスクを削除する動作を追加するには、まずリストを可変リストにします。
このために可変オブジェクト(ArrayList<T>
や mutableListOf,
など)を使用しても機能しません。これらの型は、リスト内のアイテムが変更されたことを Compose に通知せず、UI の再コンポーズをスケジュール設定しません。別の API が必要です。
Compose で監視できる MutableList
のインスタンスを作成する必要があります。この構造により、Compose は変更を追跡して、リストでアイテムを追加または削除したときに UI を再コンポーズできます。
まず、監視可能な MutableList
を定義します。拡張関数 toMutableStateList()
を使用すると、初期可変または不変の Collection
(List
など)からオブザーバブルな MutableList
を作成できます。
または、ファクトリ メソッド mutableStateListOf
を使用して監視可能な MutableList
を作成し、初期状態の要素を追加することもできます。
WellnessScreen.kt
ファイルを開きます。このファイルを使用するには、getWellnessTasks
メソッドをこのファイルに移動します。最初にgetWellnessTasks()
を呼び出してから、前に学習した拡張関数toMutableStateList
を使用してリストを作成します。
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- リストは画面レベルにホイスティングされるため、リストのデフォルト値を削除してコンポーズ可能な関数
WellnessTasksList
を変更します。新しいラムダ関数パラメータonCloseTask
を追加します(削除するWellnessTask
を受け取ります)。onCloseTask
をWellnessTaskItem
に渡します。
さらにもう 1 つ変更が必要です。items
メソッドは key
パラメータを受け取ります。デフォルトでは、各アイテムの状態には、リスト内のアイテムの位置に対応したキーが指定されます。
可変リストでは、位置が変更されたアイテムの保存された状態が実質的に失われるため、データセットが変更されたときに問題が発生します。
この問題は、各 WellnessTaskItem
の id
を各アイテムのキーとして使用すると簡単に修正できます。
リスト内のアイテムキーについて詳しくは、ドキュメントをご覧ください。
WellnessTasksList
は次のようになります。
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
WellnessTaskItem
を変更する:onClose
ラムダ関数をパラメータとしてステートフルWellnessTaskItem
に追加し、呼び出します。
@Composable
fun WellnessTaskItem(
taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
var checkedState by rememberSaveable { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = onClose,
modifier = modifier,
)
}
よくできました!機能が完成し、アイテムをリストから削除できるようになりました。
各行の [X] をクリックすると、状態を保有するリストまでイベントが移動し、リストからそのアイテムが削除され、Compose が画面を再コンポーズします。
rememberSaveable()
を使用してリストを WellnessScreen
に保存しようとすると、ランタイム例外が発生します。
このエラーは、カスタム セーバーを用意する必要があることを示しています。ただし、大量のデータや、冗長なシリアル化やシリアル化解除を必要とする複雑なデータ構造の保存には rememberSaveable
を使用しないでください。
アクティビティの onSaveInstanceState
を使用する場合も、同様のルールが適用されます。詳しくは、UI の状態を保存に関するドキュメントをご覧ください。これを行うには、代わりの保存メカニズムが必要です。UI の状態を保持するための各種オプションについて、詳しくはドキュメントをご覧ください。
次に、アプリの状態を保持するための ViewModel の役割について確認します。
12. ViewModel の状態
画面(UI 状態)は、画面に表示される内容(タスクのリストなど)を示します。この状態にはアプリのデータが含まれるため、通常は階層の他のレイヤに接続されます。
UI 状態は画面に表示するものを表しますが、アプリのロジックは、アプリがどのように動作し、状態の変化にどう反応する必要があるかを示します。ロジックには、UI 動作(UI ロジック)とビジネス ロジックの 2 種類があります。
- UI ロジックは、画面上の状態変更の表示方法(ナビゲーション ロジックやスナックバーの表示など)に関連します。
- ビジネス ロジックは、状態の変化(支払いやユーザー設定の保存など)があった場合に何を行うかを表します。このロジックは通常、UI レイヤではなくビジネスレイヤまたはデータレイヤに配置されます。
ViewModel は、UI の状態を表示し、アプリの他のレイヤにあるビジネス ロジックにアクセスできるようにします。また、ViewModel は設定変更後も持続するため、持続時間はコンポジションより長くなります。Compose Navigation を使用している場合は、Compose コンテンツのホスト(アクティビティ、フラグメント、ナビゲーション グラフのデスティネーション)のライフサイクルに従うことが可能です。
アーキテクチャと UI レイヤについて詳しくは、UI レイヤのドキュメントをご覧ください。
リストを移行してメソッドを削除する
ここまでのステップでは、コンポーズ可能な関数で状態を直接管理する方法を説明しましたが、UI ロジックとビジネス ロジックを UI 状態から切り離して、ViewModel に移行することをおすすめします。
UI の状態(リスト)を ViewModel に移行し、ビジネス ロジックの抽出も開始します。
WellnessViewModel.kt
を作成し、ViewModel クラスを追加します。
「データソース」getWellnessTasks()
を WellnessViewModel
に移動します。
前と同じように toMutableStateList
を使用して内部の _tasks
変数を定義し、tasks
をリストとして公開して、ViewModel の外部から変更できないようにします。
リストの組み込み削除関数に委任する単純な remove
関数を実装します。
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
viewModel()
関数を呼び出すと、この ViewModel に任意のコンポーザブルからアクセスできます。
この関数を使用するには、app/build.gradle.kts
ファイルを開いて以下のライブラリを追加し、Android Studio で新しい依存関係を同期します。
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
Android Studio Giraffe を使用する場合は、バージョン 2.6.2
を使用してください。それ以外の場合は、こちらでライブラリの最新バージョンを確認してください。
WellnessScreen
を開きます。Screen コンポーザブルのパラメータとしてviewModel()
を呼び出して、wellnessViewModel
ViewModel をインスタンス化します。これにより、このコンポーザブルのテスト時に置き換えて、必要に応じてホイスティングできます。WellnessTasksList
にタスクリストを渡し、onCloseTask
ラムダに対する関数を削除します。
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
viewModel()
は、既存の ViewModel
を返すか、指定されたスコープで新しく作成します。ViewModel インスタンスは、スコープが存続している限り保持されます。たとえば、コンポーザブルがアクティビティで使用されている場合、viewModel()
は、アクティビティが終了するまで、またはプロセスが強制終了されるまで、同じインスタンスを返します。
以上で終了です。ここでは ViewModel を状態の一部と統合し、ビジネス ロジックを画面に統合しました。状態はコンポジションの外部で保持され、ViewModel によって保存されるため、リストに対するミューテーションは構成の変更後も保持されます。
ViewModel は、いかなるシナリオ(システムが開始したプロセスの終了など)においてもアプリの状態を自動的には保持しません。アプリの UI の状態を維持する方法について詳しくは、ドキュメントをご覧ください。
チェック状態を移行する
最後のリファクタリングでは、チェック状態とロジックを ViewModel に移行します。これにより、コードがよりシンプルでテストしやすくなり、すべての状態が ViewModel で管理されます。
- まず、
WellnessTask
モデルクラスを変更して、チェック状態を保存し、デフォルト値として false を設定できるようにします。
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
- ViewModel でメソッド
changeTaskChecked
を実装します。このメソッドは、チェック状態の新しい値を使用して変更するタスクを受け取ります。
class WellnessViewModel : ViewModel() {
...
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
_tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
WellnessScreen
で、ViewModel のchangeTaskChecked
メソッドを呼び出して、リストのonCheckedTask
の動作を指定します。この関数は次のようになります。
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
WellnessTasksList
を開き、onCheckedTask
ラムダ関数パラメータを追加して、WellnessTaskItem.
に渡せるようにします。
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked) },
onClose = { onCloseTask(task) }
)
}
}
}
WellnessTaskItem.kt
ファイルをクリーンアップします。CheckBox の状態がリストレベルにホイスティングされるため、ステートフルなメソッドは不要になりました。このファイルには、次のコンポーズ可能な関数のみが含まれています。
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- アプリを実行し、タスクを確認します。タスクのチェックはまだ正常に機能しません。
Compose が MutableList
について追跡しているのは、要素の追加と削除に関連する変更であるためです。これが削除の仕組みです。ただし、行アイテムの値(この場合は checkedState
)の変更は、それについても追跡するよう指示しない限り、認識されません。
この問題は、次の 2 つの方法によって解決できます。
checkedState
がBoolean
ではなくMutableState<Boolean>
になるように、データクラスWellnessTask
を変更します。これにより、Compose はアイテムの変更を追跡できます。- 変更しようとしているアイテムをコピーし、リストからそのアイテムを削除して、変更後のアイテムを再度リストに追加します。これにより、Compose はそのリストの変更を追跡します。
どちらの方法にも長所と短所があります。たとえば、使用するリストの実装によっては、要素の削除や読み取りのコストが大きくなることがあります。
そこで、コストがかかる可能性のあるリスト操作を回避し、checkedState
をオブザーバブルにする方が、より効率的で、Compose に適しています。
新しい WellnessTask
は次のようになります。
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))
前述のように、委譲プロパティを使用することもできます。この場合はこの方法で変数 checked
を簡単に使用できます。
WellnessTask
をデータクラスではなくクラスに変更します。コンストラクタで WellnessTask
が initialChecked
変数をデフォルト値 false
で受け取るようにします。その後、ファクトリ メソッド mutableStateOf
を使用して checked
変数を初期化し、initialChecked
をデフォルト値として使用します。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
これで完了です。このソリューションは正常に機能し、すべての変更は再コンポーズと設定変更後も保持されます。
テスト
ビジネス ロジックは、コンポーズ可能な関数内に結合されるのではなく、ViewModel にリファクタリングされるため、単体テストが大幅に簡素化されます。
インストルメンテーション テストを使用して、Compose コードの正しい動作と、UI の状態が正しく動作しているかどうかを確認できます。Codelab「Compose でのテスト」で、Compose UI のテスト方法を学習することを検討してください。
13. 完了
よくできました!以上でこの Codelab は終了です。Jetpack Compose アプリで状態を操作するための基本 API についてすべて学習しました。
状態とイベントを意識して Compose でステートレスなコンポーザブルを抽出する方法を学び、Compose が状態の更新を使用して UI を変更する方法を学習しました。
次のステップ
Compose パスウェイに関する他の Codelab をご確認ください。
サンプルアプリ
- JetNews には、この Codelab で説明されているベスト プラクティスが記載されています。
その他のドキュメント
- Compose の思想
- 状態と Jetpack Compose
- Jetpack Compose の単方向データフロー
- Compose 内の状態を復元する
- ViewModel の概要
- Compose とその他のライブラリ