Jetpack Compose の状態

1. 始める前に

この Codelab では、Jetpack Compose の状態の使用に関する主要コンセプトについて説明します。アプリの状態から UI に表示されるものを特定する方法、複数の API を使用して状態が変わったときに Compose が UI を更新する方法、コンポーズ可能な関数の構造を最適化する方法、Compose の世界で ViewModel を使用する方法について説明します。

前提条件

学習内容

  • Jetpack Compose UI での状態とイベントの考え方。
  • Compose が状態を使用して画面に表示する要素を決定する方法。
  • 状態ホイスティングの概要。
  • ステートフルおよびステートレスなコンポーズ可能な関数の仕組み。
  • Compose が State<T> API を使用して状態を自動的に追跡する方法。
  • コンポーズ可能な関数におけるメモリと内部状態の仕組み(remember API と rememberSaveable API の使用)。
  • リストと状態を処理する方法(mutableStateListOf API と toMutableStateList API の使用)。
  • Compose で ViewModel を使用する方法。

必要なもの

推奨 / 任意

作成するアプリの概要

簡単なウェルネス アプリを実装します。

4888b02619969c55.png

このアプリには主に 2 つの機能があります。

  • 水分摂取を追跡するためのウォーター カウンタ。
  • 一日を通して行うウェルネス タスクのリスト。

この Codelab の学習を進める際のサポートとして、次の Code-Along 動画をご覧ください。

2. 設定する

新しい Compose プロジェクトを開始する

  1. 新しい Compose プロジェクトを開始するには、Android Studio を開きます。
  2. [Welcome to Android Studio] ウィンドウが開いている場合は、[Start a new Android Studio] プロジェクトをクリックします。Android Studio プロジェクトをすでに開いている場合は、メニューバーで [File] > [New] > [New Project] を選択します。
  3. 新しいプロジェクトの場合は、利用可能なテンプレートの中から [Empty Compose Activity] を選択します。

a67ba73a4f06b7ac.png

  1. [Next] をクリックし、プロジェクトを設定して、「BasicStateCodelab」という名前を付けます。

API レベル 21(API Compose がサポートする最小レベル)以上の minimumSdkVersion を選択してください。

[Empty Compose Activity] テンプレートを選択すると、Android Studio によって、プロジェクトに以下のものが設定されます。

  • 画面上にテキストを表示するコンポーズ可能な関数で構成された MainActivity クラス。
  • AndroidManifest.xml ファイル。アプリの権限、コンポーネント、カスタム リソースを定義します。
  • build.gradle ファイルと app/build.gradle ファイルには、Compose に必要なオプションと依存関係が含まれています。

Codelab の解答

この Codelab の解答コードは GitHub から入手できます。

$ git clone https://github.com/googlecodelabs/android-compose-codelabs

または、リポジトリを 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.material.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 つのセクション(ウォーター カウンタと、ウェルネス タスクのリスト)で構成されます。ここでは、カウンタを追加します。

  1. メイン画面を表す WellnessScreen.kt ファイルを作成し、WaterCounter 関数を呼び出します。
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   WaterCounter(modifier)
}
  1. 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.colors.background
               ) {
                   WellnessScreen()
               }
           }
       }
   }
}
  1. アプリを実行すると、基本的なウォーター カウンタの画面が表示され、水の量がグラスの数としてハードコードされています。

e1778709d7fb5a68.png

コンポーズ可能な関数 WaterCounter の状態は、変数 count です。しかし、静的な状態は変更できないため、あまり便利ではありません。これを修正するには、Button を追加してカウントを増やし、1 日あたりの水の摂取量をグラス数として追跡します。

状態を変更するアクションはすべて「イベント」と呼ばれます。これについては、次のセクションで詳しく説明します。

4. Compose 内のイベント

状態とは、時間の経過とともに変化する値(チャットアプリで最後に受信したメッセージなど)を指します。では、状態が更新される原因は何でしょうか。Android アプリでは、イベントに応答して状態が更新されます。

イベントは、アプリケーションの内外で生成される入力です。以下に例を示します。

  • ユーザーがボタンを押すなどの操作で UI を操作する。
  • その他の要因(センサーによる新しい値の送信、ネットワーク レスポンスなど)。

アプリの状態は UI に表示する内容を示しますが、イベントは状態を変化させるメカニズムであり、その結果 UI が変化します。

イベントは、何かが起こったことをプログラムの要素に通知します。すべての Android アプリは、次のようなコア UI 更新ループを備えています。

f415ca9336d83142.png

  • イベント – ユーザーまたはプログラムの要素によってイベントが生成されます。
  • 状態の更新 – UI で使用される状態がイベント ハンドラにより変更されます。
  • 状態の表示 – UI が更新され、新しい状態を表示します。

状態とイベントの相互作用を理解すれば、Compose の状態を扱えるようになります。

では、このボタンを追加して、ユーザーがグラスの水を追加して状態を変更できるようにします。

コンポーズ可能な関数 WaterCounter に移動し、ラベル Text の下に Button を追加します。Column を使用すると、TextButton コンポーザブルに上下に配置できます。外部パディングを Column コンポーザブルに移動し、Button の上部にパディングを追加して、テキストから分離します。

コンポーズ可能な関数 Button は、ラムダ関数 onClick を受け取ります。これは、ボタンがクリックされたときに発生するイベントです。ラムダ関数の例は後で詳しく説明します。

countval から var に変更して、可変にします。

import androidx.compose.material.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 に指示していないためです。これを次のステップで修正します。

86caa22e4f7f9072.gif

5. コンポーズ可能な関数のメモリ

Compose アプリは、コンポーズ可能な関数を呼び出すことにより、データを UI に変換します。ここでは、Compose がコンポーザブルの実行時に構築する UI の記述を「コンポジション」と呼びます。状態が変更されると、Compose は影響を受けるコンポーズ可能な関数を新しい状態で再実行し、更新された UI を作成します。これを再コンポーズと呼びます。また、Compose は個々のコンポーザブルが必要とするデータを確認するため、データが変更されたコンポーネントのみを再コンポーズし、影響を受けないコンポーネントはスキップします。

これを行うには、Compose はトラッキングする状態を認識している必要があります。これにより、更新を受信したときに再コンポーズをスケジュール設定できるようになります。

Compose には、特定の状態を読み取るすべてのコンポーザブルの再コンポーズをスケジュール設定する、特別な状態トラッキング システムが用意されています。これにより Compose で細かい作業が可能になり、UI 全体ではなく、変更が必要となるコンポーズ可能な関数を再コンポーズするだけで済みます。これを行うには、状態の書き込み(「状態の変化」)だけでなく、状態の「読み取り」も追跡します。

Compose の State 型と MutableState 型を使用して、Compose が状態を監視できるようにします。

Compose は、状態 value プロパティを読み取り、その value が変更されたときに再コンポーズをトリガーする各コンポーザブルをトラッキングします。mutableStateOf 関数を使用して、監視可能な MutableState を作成できます。初期値をパラメータとして受け取って State オブジェクトにラップすることで、value を監視できるようになります。

WaterCounter コンポーザブルを更新し、count0 を初期値として mutableStateOf API を使用するようにします。mutableStateOfMutableState 型を返すため、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 を変更すると、countvalue を自動的に読み取るコンポーズ可能な関数の再コンポーズがスケジュール設定されます。この場合、ボタンがクリックされるたびに WaterCounter が再コンポーズされます。

アプリを実行すると、今回も何も起こりません。

86caa22e4f7f9072.gif

再コンポーズのスケジュールは正常に機能しています。ただし、再コンポーズの際には、変数 count が 0 に再初期化されるため、この値を再コンポーズ後も保持する方法が必要です。

このためには、コンポーズ可能なインライン関数 remember を使用できます。remember によって計算された値は、初回コンポーズ中にコンポジションに保存され、保存された値は再コンポーズ後も保持されます。

通常、remembermutableStateOf はコンポーズ可能な関数で一緒に使用されます。

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 キーワードを使用します。委任のゲッターとセッターのインポートを追加すると、毎回 MutableStatevalue プロパティを明示的に参照せずに、間接的に 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 つ追加されます)。

これでカウンタが動作します。

c2a8eeeaa73d7315.gif

6. 状態駆動型 UI

Compose は宣言型 UI フレームワークです。UI コンポーネントの削除や、状態変化時の表示 / 非表示の変更の代わりに、UI が特定の状態条件下でどのように表示されるかを説明します。再コンポーズが呼び出されて UI が更新された場合、コンポーザブルは最終的にコンポジションに入る、またはコンポジションから出ていく可能性があります。

7d3509d136280b6c.png

このアプローチでは、ビューシステムのように手動でビューを更新する複雑な作業を回避できます。また、自動で行われるため、新しい状態に基づいて必ずビューが更新されるため、エラーが発生しにくくなります。

コンポーズ可能な関数が初回コンポーズまたは再コンポーズで呼び出される場合、その関数はコンポジション内に存在します。呼び出されないコンポーズ可能な関数(その関数が 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] ボタンをクリックして、コンポーネント ツリー全体を開きます。

右側の画面の要素をクリックすると、ツリーの対応する要素に移動します。

49ee0aa7f520832.png

[Add one] ボタンを押すと、次のようになります。

  • カウントが 1 に増え、状態が変化します。
  • 再コンポーズが呼び出されます。
  • 画面が新しい要素で再コンポーズされます。

Android Studio の Layout Inspector ツールでコンポーネント ツリーを調べると、Text コンポーザブルも表示されるようになっています。

3bde9ddfdbb1ab78.png

状態により、特定の時点でどの要素が UI に表示されるかが決まります。

UI の複数の部分が同じ状態に依存することがあります。Button を変更して、count が 10 になるまで有効となり、その後無効になるようにします(これで今日の目標は達成されます)。これを行うには、Buttonenabled パラメータを使用します。

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    ...
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
    ...
}

今すぐアプリを実行します。状態 count の変更によって、Text を表示するかどうか、および Button が有効か無効かが決まります。

e7f75abce3e3d781.gif

7. コンポジションに保存する

remember はコンポジションにオブジェクトを保存し、再コンポーズの際に remember が呼び出されるソース位置が再度呼び出されないと、そのオブジェクトを消去します。

この動作を可視化するには、アプリに次の機能を実装します。ユーザーが水を 1 杯以上飲んでいる場合は、ユーザーが行うウェルネス タスクを表示して、閉じることもできるようにします。コンポーザブルは小さくて再利用可能であるため、WellnessTaskItem という新しいコンポーザブルを作成します。これは、パラメータとして受け取った文字列に基づいてウェルネス タスクを表示し、さらに [Close] アイコンボタンを表示します。

新しいファイル WellnessTaskItem.kt を作成し、次のコードを追加します。このコンポーズ可能な関数は、この Codelab で後ほど使用します。

import androidx.compose.foundation.layout.Row
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.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 は次のようになります。

6e8b72a529e8dedd.png

機能を追加してアプリを改善するには、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")
       }
   }
}

WellnessTaskItemonClose ラムダ関数を使用して、X ボタンが押されると、変数 showTaskfalse に変更され、タスクが表示されなくなるようにします。

   ...
   WellnessTaskItem(
      onClose = { showTask = false },
      taskName = "Have you taken your 15 minute walk today?"
   )
   ...

次に、「Clear water count」というテキストがある新しい Button を追加し、「Add oneButton の横に配置します。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")
           }
       }
   }
}

アプリを実行すると、画面に初期状態が表示されます。

アプリの初期状態を示すコンポーネント図のツリー(カウントは 0)

右側には、簡素化されたコンポーネント ツリーが表示されています。これにより、状態の変化に伴う状況を分析できます。countshowTask は保存されている値です。

アプリで以下の手順を行います。

  • [Add one] ボタンを押します。これにより、count の値が増え(再コンポジションが発生します)、WellnessTaskItem とカウンタ Text の両方が表示されます。

状態変化を示すコンポーネント図のツリー。[Add one] ボタンをクリックすると、ヒント付きのテキストが表示され、グラス数のテキストが表示されます。

9257d150a5952931.png

  • WellnessTaskItem コンポーネントの X を押します(別の再コンポーズが発生します)。showTask が false になったため、WellnessTaskItem が表示されなくなります。

コンポーネントのツリー図。[Close] ボタンをクリックすると、タスク コンポーザブルが表示されなくなります。

6bf6d3991a1c9fd1.png

  • [Add one] ボタンを押します(別の再コンポーズが発生します)。グラスを追加し続けると、showTask は次の再コンポジションで WellnessTaskItem を閉じたことを記憶します。

  • [Clear water count] ボタンを押して count を 0 にリセットし、再コンポーズを行います。count を表示する Text と、WellnessTaskItem に関連するすべてのコードは呼び出されず、コンポジションから出ます。

ae993e6ddc0d654a.png

  • 保存された showTask が呼び出されるコード位置が呼び出されなかったため、showTask は破棄されました。最初の手順に戻ります。

  • [Add one] ボタンを押して、count を 0 より大きくします(再コンポーズ)。

9257d150a5952931.png

  • 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 に戻ると消えます。

34f1e94c368af98d.gif

言語を変更した場合や、ダークモードとライトモードを切り替えた場合など、Android が実行中のアクティビティを再作成するように設定するその他の変更を行った場合でも同様です。

remember を使用すると、状態は再コンポーズ後も保持されますが、設定の変更後は保持されません。そのためには、remember ではなく rememberSaveable を使用する必要があります。

rememberSaveable は、Bundle に保存可能なすべての値を自動的に保存します。その他の値については、カスタムのセーバー オブジェクトに渡すことができます。Compose での状態の復元について詳しくは、ドキュメントをご覧ください。

WaterCounter で、rememberrememberSaveable に置き換えます。

import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
        ...
        var count by rememberSaveable { mutableStateOf(0) }
        ...
}

アプリを実行して、設定を変更してみます。カウンタが適切に保存されていることを確認します。

45beaedccab5e331.gif

アクティビティの再作成は、rememberSaveable のユースケースの 1 つにすぎません。別のユースケースでは、リストを使用して作業します。

アプリの状態と UX のニーズに応じて rememberrememberSaveable のどちらを使用するかを検討してください。

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 から StatefulCountercountをホイスティングしました。

これをアプリにプラグインし、WellnessScreenStatefulCounter で更新できます。

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   StatefulCounter(modifier)
}

前述のように、状態ホイスティングにはいくつかのメリットがあります。ここでは、コードの一部を説明していきます。以下のスニペットをアプリにコピーする必要はありません

  1. ステートレスなコンポーザブルを再利用できるようになりました。 次に例を示します。

水とジュースの量をグラス単位でカウントするには、waterCountjuiceCount を保存しますが、サンプルのコンポーズ可能な関数 StatelessCounter を使用すると、2 つの異なる独立した状態を表示できます。

@Composable
fun StatefulCounter() {
    var waterCount by remember { mutableStateOf(0) }

    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}

8211bd9e0a4c5db2.png

juiceCount が変更されると、StatefulCounter が再コンポーズされます。再コンポーズの際に、Compose は juiceCount を読み取る関数を特定し、それらの関数の再コンポーズのみをトリガーします。

2cb0dcdbe75dcfbf.png

ユーザーがタップして juiceCount の値を増加させると、StatefulCounter が再コンポーズされ、juiceCount を読み取る StatelessCounter も再コンポーズされます。ただし、waterCount を読み取る StatelessCounter は再コンポーズされません。

7fe6ee3d2886abd0.png

  1. ステートフルでコンポーズ可能な関数は、複数のコンポーズ可能な関数に同じ状態を提供できます
@Composable
fun StatefulCounter() {
   var count by remember { mutableStateOf(0) }

   StatelessCounter(count, { count++ })
   AnotherStatelessMethod(count, { count *= 2 })
}

この場合、カウントが StatelessCounter または AnotherStatelessMethod で更新されると、すべてが再コンポーズされます。これは想定された結果です。

ホイスティングされた状態は共有可能なため、不要な再コンポーズを回避し、再利用性を高めるために、コンポーザブルに必要な状態のみを渡すようにしてください。

状態と状態ホイスティングについて詳しくは、Compose の状態に関するドキュメントをご覧ください。

10. リストを操作する

次に、アプリの 2 番目の機能であるウェルネス タスクのリストを追加します。リスト内のアイテムに対して実行できる操作は 2 つあります。

  • リストアイテムをチェックして、タスクを完了としてマークする。
  • 完了させるつもりがないタスクをリストから削除する。

セットアップ

  1. まず、リストアイテムを変更します。「コンポジションに保存する」の項の WellnessTaskItem を再利用し、Checkbox が含まれるように更新できます。関数をステートレスにするために、checked 状態と onCheckedChange コールバックをホイスティングしてください。

16b3672772c44395.png

このセクションの WellnessTaskItem コンポーザブルは次のようになります。

import androidx.compose.material.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")
        }
    }
}
  1. 同じファイルで、ステートフルなコンポーズ可能な関数 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,
   )
}
  1. WellnessTask.kt ファイルを作成して、ID とラベルを含むタスクをモデル化します。これをデータクラスとして定義します。
data class WellnessTask(val id: Int, val label: String)
  1. タスクのリスト自体には、WellnessTasksList.kt という名前の新しいファイルを作成し、架空のデータを生成するメソッドを追加します。
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

実際のアプリでは、データレイヤーからデータが取得されます。

  1. 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)
        }
    }
}
  1. リストを WellnessScreen に追加します。Column を使用すると、リストと既存のカウンタを縦方向に揃えることができます。
import androidx.compose.foundation.layout.Column

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()
       WellnessTasksList()
   }
}
  1. アプリを実行して、試してみましょう。タスクの確認はできるようになりましたが、削除はできません。これは後のセクションで実装します。

2903e244176c2dda.gif

LazyList でアイテムの状態を復元する

ここでは、WellnessTaskItem コンポーザブルの詳細について説明します。

checkedState は、プライベート変数と同様に、各 WellnessTaskItem コンポーザブルに独立して属します。checkedState を変更すると、LazyColumn 内のすべての WellnessTaskItem インスタンスではなく、WellnessTaskItem のそのインスタンスのみが再コンポーズされます。

次の手順に沿って試してください。

  1. このリストの上部にある要素(要素 1、要素 2 など)をオンにします。
  2. リストの一番下までスクロールして、これらの要素が画面の外に出るようにします。
  3. 一番上までスクロールして、先ほどオンにした項目を表示します。
  4. オフになっていることを確認します。

前のセクションで確認したように、アイテムがコンポジションから出ると、保存された状態が破棄されるという問題があります。LazyColumn 上のアイテムの場合、スクロールするとアイテムはコンポジションから離れてしまい、表示されなくなります。

d3c12f57cc98db16.gif

どうすれば解決できるでしょうか。ここでも、rememberSaveable を使用します。保存された値は、保存されたインスタンス状態メカニズムを使用するアクティビティまたはプロセスの再作成の(この場合は、アイテムがコンポジションを離れた)後も存続します。

ステートフル WellnessTaskItemrememberrememberSaveable に置き換えるだけです。

import androidx.compose.runtime.saveable.rememberSaveable

var checkedState by rememberSaveable { mutableStateOf(false) }

9b4ba365481ae97a.gif

Compose の一般的なパターン

LazyColumn の実装に注意してください。

@Composable
fun LazyColumn(
...
    state: LazyListState = rememberLazyListState(),
...

コンポーズ可能な関数 rememberLazyListState は、rememberSaveable を使用してリストの初期状態を作成します。アクティビティが再作成されると、スクロール状態は維持されます。コーディングは必要ありません。

多くのアプリでは、スクロール位置、アイテム レイアウトの変更、リストの状態に関連するその他のイベントに反応してリッスンする必要があります。LazyColumnLazyRow などの遅延コンポーネントは、LazyListState をホイスティングすることでこのユースケースをサポートします。このパターンについて詳しくは、リストの状態に関するドキュメントをご覧ください。

パブリックな rememberX 関数によりデフォルト値の状態パラメータを使用することは、組み込みのコンポーズ可能な関数では一般的なパターンです。別の例は、Scaffold にあります。これは rememberScaffoldState を使用して状態をホイスティングします。

11. オブザーバブルな MutableList

次に、リストからタスクを削除する動作を追加するには、まずリストを可変リストにします。

このために可変オブジェクト(ArrayList<T>mutableListOf, など)を使用しても機能しません。これらの型は、リスト内のアイテムが変更されたことを Compose に通知せず、UI の再コンポーズをスケジュール設定しません。別の API が必要です。

Compose で監視できる MutableList のインスタンスを作成する必要があります。この構造により、Compose は変更を追跡して、リストでアイテムを追加または削除したときに UI を再コンポーズできます。

まず、監視可能な MutableList を定義します。拡張関数 toMutableStateList() を使用すると、初期可変または不変の CollectionList など)からオブザーバブルな MutableList を作成できます。

または、ファクトリ メソッド mutableStateListOf を使用して監視可能な MutableList を作成し、初期状態の要素を追加することもできます。

  1. 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") }
  1. リストは画面レベルにホイスティングされるため、リストのデフォルト値を削除してコンポーズ可能な関数 WellnessTaskList を変更します。新しいラムダ関数パラメータ onCloseTask を追加します(削除する WellnessTask を受け取ります)。onCloseTaskWellnessTaskItem に渡します。

さらにもう 1 つ変更が必要です。items メソッドは key パラメータを受け取ります。デフォルトでは、各アイテムの状態には、リスト内のアイテムの位置に対応したキーが指定されます。

可変リストでは、位置が変更されたアイテムの保存された状態が実質的に失われるため、データセットが変更されたときに問題が発生します。

この問題は、各 WellnessTaskItemid を各アイテムのキーとして使用すると簡単に修正できます。

リスト内のアイテムキーについて詳しくは、ドキュメントをご覧ください。

WellnessTaskList は次のようになります。

@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) })
       }
   }
}
  1. 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 が画面を再コンポーズします。

47f4a64c7e9a5083.png

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 の状態(リスト)を ViewModel に移行し、ビジネス ロジックの抽出も開始します。

  1. 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") }
  1. viewModel() 関数を呼び出すと、この ViewModel に任意のコンポーザブルからアクセスできます。

この関数を使用するには、app/build.gradle ファイルを開いて以下のライブラリを追加し、Android Studio で新しい依存関係を同期します。

implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.1"
  1. 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 で管理されます。

  1. まず、WellnessTask モデルクラスを変更して、チェック状態を保存し、デフォルト値として false を設定できるようにします。
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
  1. ViewModel でメソッド changeTaskChecked を実装します。このメソッドは、チェック状態の新しい値を使用して変更するタスクを受け取ります。
class WellnessViewModel : ViewModel() {
   ...
   fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
       tasks.find { it.id == item.id }?.let { task ->
           task.checked = checked
       }
}
  1. 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)
           }
       )
   }
}
  1. 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) }
           )
       }
   }
}
  1. 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")
       }
   }
}
  1. アプリを実行し、タスクを確認します。タスクのチェックはまだ正常に機能しません。

8e2e731c58123cd8.gif

これは、Compose が MutableList について追跡している対象が、要素の追加と削除に関連する変更であるためです。これが削除の仕組みです。ただし、行アイテムの値(この場合は checkedState)の変更は、それについても追跡するよう指示しない限り、認識されません。

この問題は、次の 2 つの方法によって解決できます。

  • checkedStateBoolean ではなく 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 をデータクラスではなくクラスに変更します。コンストラクタで WellnessTaskinitialChecked 変数をデフォルト値 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)
}

これで完了です。このソリューションは正常に機能し、すべての変更は再コンポーズと設定変更後も保持されます。

c8e049d56e7b0eac.gif

テスト

ビジネス ロジックは、コンポーズ可能な関数内に結合されるのではなく、ViewModel にリファクタリングされるため、単体テストが大幅に簡素化されます。

インストゥルメント化テストを使用して、Compose コードの正しい動作と、UI の状態が正しく動作しているかどうかを確認できます。Codelab「Compose でのテスト」で、Compose UI のテスト方法を学習することを検討してください。

13. 完了

よくできました!以上でこの Codelab は終了です。Jetpack Compose アプリで状態を操作するための基本 API についてすべて学習しました。

状態とイベントを意識して Compose でステートレスなコンポーザブルを抽出する方法を学び、Compose が状態の更新を使用して UI を変更する方法を学習しました。

次のステップ

Compose パスウェイに関する他の Codelab をご確認ください。

サンプルアプリ

  • JetNews には、この Codelab で説明されているベスト プラクティスが記載されています。

その他のドキュメント

リファレンス API