Compose の状態の概要

1. 始める前に

この Codelab では、状態の概要と、Jetpack Compose での状態の使用方法と操作方法について説明します。

本質的には、アプリの状態とは、時間の経過とともに変化する可能性がある値を指します。この定義は非常に広範であり、データベースからアプリ内の変数まですべてが含まれます。データベースについては後のユニットで詳しく説明しますが、ここで覚えておくべきことは、データベースは構造化された情報(パソコン上のファイルなど)をまとめたものであることです。

すべての Android アプリはユーザーに状態を表示します。Android アプリの状態の例を次にいくつか示します。

  • ネットワーク接続を確立できないときに表示されるメッセージ。
  • フォーム(登録フォームなど)。状態を記入して送信できます。
  • ボタンなどのタップ可能なコントロール。状態は、タップなし、タップ中(ディスプレイ アニメーション)、またはタップ済みonClick アクション)のいずれかです。

この Codelab では、Compose を使用する際の状態の考え方と使い方を学びます。そのために、以下の組み込みの Compose UI 要素を使用して Tip Time というチップ計算アプリをビルドします。

  • テキストの入力と編集を行う TextField コンポーザブル。
  • テキストを表示する Text コンポーザブル。
  • UI 要素間に空白スペースを表示する Spacer コンポーザブル。

この Codelab の最後には、サービス料金を入力するとチップ金額が自動的に計算されるインタラクティブなチップ計算ツールが完成します。以下の画像は最終的なアプリの外観を示しています。

d6c6ed627ffa4.png

前提条件

  • Compose に関する基礎知識(@Composable アノテーションなど)。
  • RowColumn のレイアウト コンポーザブルなど、Compose レイアウトに関する基本的な知識。
  • Modifier.padding() 関数など、修飾子に関する基本的な知識。
  • Text コンポーザブルに精通していること。

学習内容

  • UI で状態について検討する方法。
  • Compose が状態を使用してデータを表示する方法。
  • テキスト ボックスをアプリに追加する方法。
  • 状態をホイスティングする方法。

作成するアプリの概要

  • サービス料金に基づいてチップ金額を計算するチップ計算アプリ「Tip Time」。

必要なもの

  • ウェブブラウザがインストールされた、インターネットに接続できるパソコン
  • Kotlin に関する知識
  • Android Studio の最新バージョン

2. 始める

  1. Google のオンライン チップ計算ツールをご覧ください。これはあくまで一例であり、このコースで作成する Android アプリではありません。

b7d1ae0f60c4ba2e.png 19b877bbeca9ef9.png

  1. [Bill] ボックスと [Tip %] ボックスに異なる値を入力します。チップの金額と総額が変わります。

c793ff18ad2060e9.png

値を入力するとすぐに、[Tip] と [Total] が更新されます。次の Codelab を修了すると、同様のチップ計算アプリを Android で開発できるようになります。

ここでは、簡単なチップ計算ツールを Android アプリとして作成します。

デベロッパーは多くの場合、このように(外観はともかく)正常に動作するアプリの簡易版を作成します。そのうえで機能を追加し、デザインを洗練されたものにします。

この Codelab の終わりには、チップ計算アプリは次のスクリーンショットのようになります。ユーザーが請求額を入力すると、チップの推奨金額が表示されます。ここでは、チップの割合は 15% にハードコードされています。次の Codelab では、引き続きアプリで作業し、チップの割合のカスタム設定などの機能を追加します。

3. スターター コードを取得

スターター コードは、新しいプロジェクトの出発点として使用できる事前に作成されたコードです。また、この Codelab で学んだ新しいコンセプトに集中することもできます。

スターター コードはこちらからダウンロードできます。

または、GitHub リポジトリのクローンを作成してコードを入手することもできます。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout starter

スターター コードは TipTime GitHub リポジトリで確認できます。

スターター アプリの概要

次の手順でスターター コードを確認し、よく理解してください。

  1. Android Studio でスターター コードのプロジェクトを開きます。
  2. Android デバイスまたはエミュレータでアプリを実行します。
  3. 2 つのテキスト コンポーネントが表示されます。1 つはラベル用、もう 1 つはチップ金額を表示するためのものです。

78e9ba2ba645b19e.png

スターター コードのチュートリアル

スターター コードにはテキスト コンポーザブルが含まれています。このパスウェイでは、ユーザーが入力できるようにテキスト フィールドを追加します。作業の土台とするファイルの一部について簡単に説明します。

[res] > [values] > [strings.xml]

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="bill_amount">Bill Amount</string>
   <string name="tip_amount">Tip Amount: %s</string>
</resources>

これは、このアプリで使用するすべての文字列を含むリソースの string.xml ファイルです。

MainActivity

このファイルには、主にテンプレートから生成されたコードと次の関数が含まれています。

  • TipTimeLayout() 関数には、スクリーンショットに示されている 2 つのテキスト コンポーザブルを持つ Column 要素が含まれています。また、美観上の理由でスペースを追加する spacer コンポーザブルもあります。
  • 請求額を受け取って 15% のチップ金額を計算する calculateTip() 関数。tipPercent パラメータは 15.0 のデフォルト引数値に設定されています。これによって現時点で、デフォルトのチップ値が 15% に設定されます。次の Codelab では、ユーザーからチップ金額を取得します。
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.Start)
        )
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        Spacer(modifier = Modifier.height(150.dp))
    }
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

onCreate() 関数の Surface() ブロックで、TipTimeLayout() 関数が呼び出されています。デバイスまたはエミュレータにアプリのレイアウトが表示されます。

override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeLayout()
           }
       }
   }
}

TipTimeLayoutPreview() 関数の TipTimeTheme ブロックでは、TipTimeLayout() 関数が呼び出されています。これにより、[Design] ペインと [Split] ペインにアプリのレイアウトが表示されます。

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
   TipTimeTheme {
       TipTimeLayout()
   }
}

83ddead6f1179fbc.png

ユーザーの入力を受け取る

このセクションでは、ユーザーがアプリで請求額を入力できる UI 要素を追加します。下の画像のように表示されます。

cc51b428369a893d.png

このアプリはカスタムのスタイルとテーマを使用します。

スタイルとテーマは、単一の UI 要素の外観を指定する属性の集まりです。スタイルでは、アプリ全体に適用できるフォントの色、フォントサイズ、背景色などの属性を指定できます。これらをアプリに実装する方法については、後の Codelab で説明します。現時点では、アプリの外観を美しくするためにすでに対応済みです。

よりわかりやすくするために、カスタムテーマを使用した場合と使用しない場合のアプリのソリューション バージョンを以下に並べます。

カスタムテーマを使用しない場合。

カスタムテーマを使用した場合。

コンポーズ可能な関数 TextField を使用すると、ユーザーはアプリにテキストを入力できます。たとえば、次の画像に示すように、Gmail アプリのログイン画面にテキスト ボックスが配置されています。

メール用のテキスト フィールドを含む Gmail アプリのスマートフォンの画面

TextField コンポーザブルをアプリに追加します。

  1. MainActivity.kt ファイルに、Modifier パラメータを受け取るコンポーズ可能な関数 EditNumberField() を追加します。
  2. TipTimeLayout() の下の EditNumberField() 関数の本体で、空の文字列に設定される value 名前付きパラメータと、空のラムダ式に設定される onValueChange 名前付きパラメータを受け入れる TextField を追加します。
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   TextField(
      value = "",
      onValueChange = {},
      modifier = modifier
   )
}
  1. 渡したパラメータに注意してください。
  • value パラメータは、ここで渡す文字列値を表示するテキスト ボックスです。
  • onValueChange パラメータは、ユーザーがテキスト ボックスにテキストを入力するとトリガーされるラムダ コールバックです。
  1. この関数をインポートします。
import androidx.compose.material3.TextField
  1. TipTimeLayout() コンポーザブルで、最初のテキスト コンポーズ可能な関数の後の行で、EditNumberField() 関数を呼び出して次の修飾子を渡します。
import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun TipTimeLayout() {
   Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
   ) {
       Text(
           ...
       )
       EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
       Text(
           ...
       )
       ...
   }
}

これにより、画面にテキスト ボックスが表示されます。

  1. [Design] ペインで、Calculate Tip テキスト、空のテキスト ボックス、Tip Amount テキスト コンポーザブルが表示されます。

2f2ef25c956e357f.png

4. Compose で状態を使用する

アプリにおいて状態とは、時間とともに変化する可能性がある値を指します。このアプリでは、状態は請求額を表します。

状態を保存する変数を追加します。

  1. EditNumberField() 関数の先頭で、val キーワードを使用して amountInput 変数を追加し、"0" 値に設定します。
val amountInput = "0"

請求額に対するアプリの状態です。

  1. value 名前付きパラメータを amountInput 値に設定します。
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. プレビューを確認します。次の画像に示すように、テキスト ボックスに状態変数に設定された値が表示されます。

ecbf5f5015668e.png

  1. エミュレータでアプリを実行し、別の値を入力してください。TextField コンポーザブルは自身で更新しないため、ハードコードされた状態は変更されません。amountInput プロパティに設定されている value パラメータが変更されると更新されます。

amountInput 変数は、テキスト ボックスの状態を表します。ハードコードされた状態は、変更できず、ユーザー入力を反映しないため、有用ではありません。ユーザーが請求額を更新したときに、アプリの状態を更新する必要があります。

5. Composition

アプリのコンポーザブルは、テキスト、スペーサー、テキスト ボックスを含む列を表示する UI を記述します。テキストには Calculate Tip テキストが表示され、テキスト ボックスには 0 値またはデフォルト値のいずれかが表示されます。

Compose は宣言型 UI フレームワークであり、コードで UI をどのように表示するかを宣言します。最初にテキスト ボックスに 100 値を表示する場合は、コンポーザブルのコードの初期値を 100 値に設定します。

アプリの実行中に、またはユーザーがアプリを操作したときに、UI を変更する必要がある場合はどうしますか。たとえば、ユーザーが入力した値で amountInput 変数を更新しテキスト ボックスに表示する場合はどうでしょうか。その場合は、再コンポーズと呼ばれるプロセスを利用してアプリの Composition を更新します。

Composition は、コンポーザブルの実行時に Compose がビルドする UI の記述です。Compose アプリは、コンポーズ可能な関数を呼び出して、データを UI に変換します。状態が変更されると、Compose は影響を受けるコンポーズ可能な関数を新しい状態で再実行し、更新された UI を作成します。これを再コンポーズと呼びます。Compose は、再コンポーズをスケジュール設定します。

Compose は、初回コンポーズで初めてコンポーザブルを実行する際に、Composition の UI を記述するために呼び出されるコンポーザブルをトラッキングします。再コンポーズの際、Compose はデータの変化に応じて変化した可能性があるコンポーザブルを再実行し、変化を反映するために Composition を更新します。

Composition は、初回コンポーズによってのみ作成され、再コンポーズによってのみ更新されます。Composition を変更する唯一の方法は、再コンポーズを行うことです。これを行うには、Compose はトラッキングする状態を認識している必要があります。これにより、更新を受信したときに再コンポーズをスケジュール設定できるようになります。今回は amountInput 変数であるため、値が変更されるたびに、Compose は再コンポーズをスケジュール設定します。

Compose でアプリの状態を監視またはトラッキングできるようにするには、Compose で State 型と MutableState 型を使用します。State 型は変更不可であるため、その中の値の読み取りのみが可能ですが、MutableState 型は変更可能です。mutableStateOf() 関数を使用して、監視可能な MutableState を作成できます。初期値をパラメータとして受け取って State オブジェクトにラップすることで、value を監視できるようになります。

mutableStateOf() 関数により返される値です。

  • 状態(請求額)を保持します。
  • 可変であるため、値を変更できます。
  • 監視可能であるため、Compose は値の変更を監視し、UI を更新するための再コンポーズをトリガーします。

サービス料金の状態を追加します。

  1. EditNumberField() 関数で、amountInput 状態変数の前の val キーワードを var キーワードに変更します。
var amountInput = "0"

これにより amountInput が可変になります。

  1. Compose が amountInput の状態をトラッキングしてから、amountInput 状態変数のデフォルトの初期値である "0" 文字列を渡すことを認識するように、ハードコードされた String 変数ではなく MutableState<String> 型を使用します。
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

var amountInput: MutableState<String> = mutableStateOf("0")

amountInput の初期化は、型推論を使用して次のように記述することもできます。

var amountInput = mutableStateOf("0")

mutableStateOf() 関数は最初の "0" 値を引数として受け取り、amountInput を監視できるようにします。その結果、Android Studio でこのコンパイルの警告が表示されますが、この後すぐに解決します。

Creating a state object during composition without using remember.
  1. コンポーズ可能な関数 TextField で、amountInput.value プロパティを使用します。
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

Compose は、状態 value プロパティを読み取り、その value が変更されたときに再コンポーズをトリガーする各コンポーザブルをトラッキングします。

onValueChange コールバックは、テキスト ボックスの入力が変更されるとトリガーされます。ラムダ式では、it 変数に新しい値が含まれています。

  1. onValueChange 名前付きパラメータのラムダ式で、amountInput.value プロパティを it 変数に設定します。
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
       modifier = modifier
   )
}

onValueChange コールバック関数によってテキストの変更が行われたことが TextField から通知された際に、TextField の状態(amountInput 変数)を更新します。

  1. アプリを実行し、テキスト ボックスにテキストを入力します。下の画像で確認できるように、テキスト ボックスには引き続き値 0 が表示されています。

3a2c62f8ec55e339.gif

ユーザーがテキスト ボックスにテキストを入力すると、onValueChange コールバックが呼び出され、amountInput 変数が新しい値で更新されます。amountInput 状態は Compose によってトラッキングされるため、値が変更されると再コンポーズがスケジュール設定され、コンポーズ可能な関数 EditNumberField() が再度実行されます。そのコンポーズ可能な関数では、amountInput 変数は最初の 0 値にリセットされます。このようにして、テキスト ボックスに 0 値が表示されます。

追加したコードでは、状態の変更によって再コンポーズがスケジュール設定されます。

ただし、EditNumberField() 関数が再コンポーズされるたびに 0 値にリセットされないように、再コンポーズ全体で amountInput 変数の値を保持する必要があります。この問題は次のセクションで解決します。

6. remember 関数を使用して状態を保存する

再コンポーズにより、コンポーズ可能なメソッドが何度も呼び出される場合があります。コンポーザブルは、保存されていない場合、再コンポーズ中に状態をリセットします。

コンポーズ可能な関数は、remember を使用して再コンポーズ全体でオブジェクトを格納できます。初回コンポーズの際に、remember 関数によって計算された値が Composition に保存され、保存された値は再コンポーズの際に返されます。通常、remember 関数と mutableStateOf 関数はコンポーズ可能な関数で併用し、状態とその更新が UI に適切に反映されるようにします。

EditNumberField() 関数で remember 関数を使用します。

  1. EditNumberField() 関数で、mutableStateOf() 関数の呼び出しを remember で囲んで、by remember Kotlin プロパティのデリゲートを使用して amountInput 変数を初期化します。
  2. mutableStateOf() 関数で、静的な "0" 文字列ではなく、空の文字列を渡します。
var amountInput by remember { mutableStateOf("") }

これで、空の文字列が amountInput 変数の初期デフォルト値になります。byKotlin のプロパティ委任です。amountInput プロパティのデフォルトのゲッター関数とセッター関数は、それぞれ remember クラスのゲッター関数とセッター関数に委任されています。

  1. 次の関数をインポートします。
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

委任のゲッターとセッターのインポートを追加すると、MutableStatevalue プロパティを参照せずに amountInput を読み取り、設定できます。

更新された EditNumberField() 関数は次のようになります。

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       modifier = modifier
   )
}
  1. アプリを実行し、テキスト ボックスにテキストを入力します。入力したテキストが表示されます。

f60dddc9dcf03edf.png

7. 状態と再コンポーズの実例

このセクションでは、ブレークポイントを設定してコンポーズ可能な関数 EditNumberField() をデバッグし、初期コンポーズと再コンポーズの仕組みを確認します。

エミュレータまたはデバイスでブレークポイントを設定し、アプリをデバッグします。

  1. EditNumberField() 関数内の、onValueChange 名前付きパラメータの横にある行ブレークポイントを設定します。
  2. ナビゲーション メニューで、[Debug 'app'] をクリックします。アプリがエミュレータまたはデバイスで起動します。TextField 要素が最初に作成されたときに、アプリの実行が一時停止します。

e2e1541f22e39281.png

  1. [Debug] ペインで、7bdc150b4ddfdab3.png [Resume Program] をクリックします。テキスト ボックスが作成されます。
  2. エミュレータまたはデバイスで、テキスト ボックスに文字を入力します。設定したブレークポイントに到達すると、アプリの実行は再び一時停止します。

テキストを入力すると、onValueChange コールバックが呼び出されます。ラムダ it 内に、キーパッドで入力した新しい値があります。

「it」の値が amountInput に割り当てられた後、オブザーバブルな値が変更されると、Compose は新しいデータを使用し再コンポーズをトリガーします。

987b5951f9f33262.png

  1. [Debug] ペインで、7bdc150b4ddfdab3.png [Resume Program] をクリックします。エミュレータまたはデバイスで入力したテキストは、この画像に示すように、ブレークポイントがある行の横に表示されます。

7e7a3c1a4a64e987.png

これはテキスト フィールドの状態です。

  1. 7bdc150b4ddfdab3.png [Resume Program] をクリックします。入力した値がエミュレータまたはデバイスに表示されます。

8. 外観を変更する

前のセクションでは、テキスト フィールドが機能するように設定しました。このセクションでは、UI を拡張します。

テキスト ボックスにラベルを追加する

すべてのテキスト ボックスに、ユーザーが入力できる情報を示すラベルを設定する必要があります。次のサンプル画像の最初の部分では、ラベルテキストがテキスト フィールドの中央にあり、入力行に沿って表示されています。次のサンプル画像の 2 番目の部分では、ユーザーがテキスト ボックスをクリックしてテキストを入力すると、テキスト ボックス内のラベルが上に移動します。テキスト フィールドの構造について詳しくは、構造をご覧ください。

9e802ed30b7612b0.png

EditNumberField() 関数を変更して、テキスト フィールドにラベルを追加します。

  1. EditNumberField() 関数のコンポーズ可能な関数 TextField() で、label 名前付きパラメータを追加して空のラムダ式に設定します。
TextField(
//...
   label = { }
)
  1. ラムダ式では、stringResource(R.string.bill_amount) を受け入れる Text() 関数を呼び出します。
label = { Text(stringResource(R.string.bill_amount)) },
  1. コンポーズ可能な関数 TextField() で、singleLine 名前付きパラメータを追加して true 値に設定します。
TextField(
  // ...
   singleLine = true,
)

これにより、テキスト ボックスが複数の行から水平方向にスクロールできる 1 行にまとめられます。

  1. keyboardOptions パラメータを追加して KeyboardOptions() に設定します。
import androidx.compose.foundation.text.KeyboardOptions

TextField(
  // ...
   keyboardOptions = KeyboardOptions(),
)

Android の各種画面では、数字、メールアドレス、URL、パスワードなどの入力用に画面に表示するキーボードを必要に応じて構成できます。他のキーボード タイプについて詳しくは、KeyboardType をご覧ください。

  1. キーボード タイプを数字キーボードに設定し、数字を入力します。KeyboardOptions 関数に keyboardType 名前付きパラメータを渡し KeyboardType.Number に設定します。
import androidx.compose.ui.text.input.KeyboardType

TextField(
  // ...
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)

完成した EditNumberField() 関数は次のコード スニペットのようになります。

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
    var amountInput by remember { mutableStateOf("") }
    TextField(
        value = amountInput,
        onValueChange = { amountInput = it },
        singleLine = true,
        label = { Text(stringResource(R.string.bill_amount)) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        modifier = modifier
    )
}
  1. アプリを実行します。

キーパッドの変更点を次のスクリーンショットに示します。

bbd4c90747fb8d28.png

9. チップの金額を表示する

このセクションでは、チップ金額の計算と表示という、アプリの主な機能を実装します。

MainActivity.kt ファイルでは、スターター コードの一部として private calculateTip() 関数が提供されています。この関数を使用してチップ金額を計算します。

private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
    val tip = tipPercent / 100 * amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

上記のメソッドでは、NumberFormat を使用してチップの表示形式を通貨として表示しています。

これで、アプリがチップを計算できるようになりましたが、クラスを使用して金額の書式設定と表示を行う必要があります。

calculateTip() 関数を使用する

ユーザーがテキスト フィールド コンポーザブルに入力したテキストは、ユーザーが数値を入力した場合でも String として onValueChange コールバック関数に返されます。この問題を解決するには、ユーザーが入力した金額を含む amountInput 値を変換する必要があります。

  1. コンポーズ可能な関数 EditNumberField() で、amountInput 定義の後に amount という新しい変数を作成します。amountInput 変数で toDoubleOrNull 関数を呼び出して、StringDouble に変換します。
val amount = amountInput.toDoubleOrNull()

toDoubleOrNull() 関数は、文字列を Double 数値として解析し、結果または null(文字列が数値の有効な表現でない場合)を返す、事前定義された Kotlin 関数です。

  1. ステートメントの最後に、amountInput が null の場合に 0.0 値を返す ?: Elvis 演算子を追加します。
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. amount 変数の後に、tip という別の val 変数を作成します。calculateTip() で初期化し、amount パラメータを渡します。
val tip = calculateTip(amount)

完成した EditNumberField() 関数は次のコード スニペットのようになります。

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.bill_amount)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

計算されたチップ金額を表示する

チップ金額を計算する関数を記述したら、次のステップでは計算されたチップ金額を表示します。

  1. Column() ブロックの最後にある TipTimeLayout() 関数で、$0.00 を表示するテキスト コンポーザブルに注目してください。これを計算されたチップ金額に更新します。
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // ...
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        // ...
    }
}

チップ金額を計算して表示するには、TipTimeLayout() 関数の amountInput 変数にアクセスする必要がありますが、amountInput 変数はコンポーズ可能な関数 EditNumberField() で定義されたテキスト フィールドの状態であるため、TipTimeLayout() 関数からはまだ呼び出すことができません。以下の画像は、コードの構造を示しています。

4d8b69d49a90683a.png

この構造体では、新しい Text コンポーザブルは amountInput 変数から計算された amount 変数にアクセスする必要があるため、Text コンポーザブルにチップ金額を表示することはできません。amount 変数を TipTimeLayout() 関数に公開する必要があります。以下の画像は、目的のコード構造を示しています。これにより、EditNumberField() コンポーザブルはステートレスになります。

38bd92a2346a910b.png

このパターンは、状態ホイスティングと呼ばれます。次のセクションでは、コンポーザブルから状態をホイスティング(リフト)して、ステートレスにします。

10. 状態ホイスティング

このセクションでは、コンポーザブルを再利用して共有できるように状態を定義する場所を決定する方法について説明します。

コンポーズ可能な関数で、UI に表示する状態を保持する変数を定義できます。例としては、amountInput 変数を EditNumberField() コンポーザブルの状態として定義したことが挙げられます。

アプリが複雑になり、他のコンポーザブルが EditNumberField() コンポーザブル内の状態にアクセスする必要がある場合は、コンポーズ可能な関数 EditNumberField() から状態をホイスティング(抽出)することを検討する必要があります。

ステートフルとステートレスなコンポーザブルを理解する

次の場合は、状態をホイスティングする必要があります。

  • 状態を複数のコンポーズ可能な関数と共有する。
  • アプリで再利用できるステートレスなコンポーザブルを作成する。

コンポーズ可能な関数から状態を抽出する場合、作成されるコンポーズ可能な関数はステートレスと呼ばれます。つまり、コンポーズ可能な関数は、それらの状態を抽出してステートレスにできます。

ステートレスなコンポーザブルとは、状態を保持しない(つまり、新しい状態を保持、定義、変更しない)コンポーザブルです。一方、ステートフルなコンポーザブルとは、時間とともに変化する可能性がある状態を所有するコンポーザブルです。

状態ホイスティングは、状態を呼び出し元に移行してコンポーネントをステートレスにするパターンです。

コンポーザブルに状態ホイスティングを適用すると、多くの場合、コンポーザブルに 2 つのパラメータを導入することになります。

  • value: T パラメータ。表示する現在の値です。
  • onValueChange: (T) -> Unit - コールバック ラムダ。値が変更されたときにトリガーされ、ユーザーがテキスト ボックスにテキストを入力したときなど、状態を他の場所で更新できるようにします。

EditNumberField() 関数で状態をホイスティングします。

  1. EditNumberField() 関数定義を更新し、value パラメータと onValueChange パラメータを追加して状態をホイスティングします。
@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
//...

value パラメータは String 型、onValueChange パラメータは (String) -> Unit 型であるため、String 値を入力として受け取り、戻り値が存在しない関数です。onValueChange パラメータは、TextField コンポーザブルに渡される onValueChange コールバックとして使用されます。

  1. EditNumberField() 関数で、渡されたパラメータを使用するようにコンポーズ可能な関数 TextField() を更新します。
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. 状態をホイスティングし、remember で保存した状態を EditNumberField() 関数から TipTimeLayout() 関数に移動します。
@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       //...
   ) {
       //...
   }
}
  1. 状態を TipTimeLayout() にホイスティングしたら、次に EditNumberField() に渡します。TipTimeLayout() 関数で、ホイスティングした状態を使用するように EditNumberField() 関数の呼び出しを更新します。
EditNumberField(
   value = amountInput,
   onValueChange = { amountInput = it },
   modifier = Modifier
       .padding(bottom = 32.dp)
       .fillMaxWidth()
)

これにより、EditNumberField がステートレスになります。UI 状態を祖先である TipTimeLayout() にホイスティングしました。TipTimeLayout() が状態(amountInput)のオーナーになりました。

位置による書式設定

動的コンテンツを文字列で表示するために、位置による書式設定が使用されます。たとえば、[Tip amount] テキスト ボックスに xx.xx 値(関数で計算、書式設定された任意の金額)を表示するとします。strings.xml ファイルでこれを実現するには、次のコード スニペットのように、プレースホルダ引数を使用して文字列リソースを定義する必要があります。

// No need to copy.

// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>

Compose コードでは、任意の型のプレースホルダ引数を複数指定できます。string プレースホルダは %s です。

TipTimeLayout() のテキスト コンポーザブルに注目すると、書式設定されたチップを stringResource() 関数に引数として渡します。

// No need to copy
Text(
   text = stringResource(R.string.tip_amount, "$0.00"),
   style = MaterialTheme.typography.displaySmall
)
  1. 関数 TipTimeLayout() で、tip プロパティを使用してチップ金額を表示します。tip 変数をパラメータとして使用するように、Text コンポーザブルの text パラメータを更新します。
Text(
     text = stringResource(R.string.tip_amount, tip),
     // ...

完成した TipTimeLayout() 関数と EditNumberField() 関数は、次のコード スニペットのようになります。

@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }
   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
       horizontalAlignment = Alignment.CenterHorizontally,
       verticalArrangement = Arrangement.Center
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           modifier = Modifier
               .padding(bottom = 16.dp, top = 40.dp)
               .align(alignment = Alignment.Start)
       )
       EditNumberField(
           value = amountInput,
           onValueChange = { amountInput = it },
           modifier = Modifier
               .padding(bottom = 32.dp)
               .fillMaxWidth()
       )
       Text(
           text = stringResource(R.string.tip_amount, tip),
           style = MaterialTheme.typography.displaySmall
       )
       Spacer(modifier = Modifier.height(150.dp))
   }
}

@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   TextField(
       value = value,
       onValueChange = onValueChange,
       singleLine = true,
       label = { Text(stringResource(R.string.bill_amount)) },
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
       modifier = modifier
   )
}

要約すると、amountInput 状態を EditNumberField() から TipTimeLayout() コンポーザブルにホイスティングしました。テキスト ボックスをこれまでと同じように動作させるには、コンポーズ可能な関数 EditNumberField()amountInput 値と、ユーザーの入力によって amountInput 値を更新するラムダ コールバックの 2 つの引数を渡す必要があります。これらの変更により、TipTimeLayout()amountInput プロパティからチップを計算してユーザーに表示できるようになります。

  1. エミュレータまたはデバイスでアプリを実行し、[Bill Amount] テキスト ボックスに値を入力します。請求額の 15% となるチップ金額が次の画像のように表示されます。

b6bd5374911410ac.png

11. 解答コードを取得する

この Codelab の完成したコードをダウンロードするには、以下の git コマンドを使用します。

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout state

または、リポジトリを ZIP ファイルとしてダウンロードし、Android Studio で開くこともできます。

解答コードを確認する場合は、GitHub で表示します

12. おわりに

これで、この Codelab は終了です。Compose アプリで状態を使用する方法を学習しました。

まとめ

  • アプリにおいて状態とは、時間とともに変化する可能性がある値を指します。
  • Composition は、コンポーザブルの実行時に Compose がビルドする UI の記述です。Compose アプリは、コンポーズ可能な関数を呼び出して、データを UI に変換します。
  • 初回コンポーズとは、Compose がコンポーズ可能な関数を初めて実行する際に UI を作成することです。
  • 再コンポーズは、データが変更されたときに同じコンポーザブルを再度実行してツリーを更新する処理です。
  • 状態ホイスティングは、状態を呼び出し元に移行してコンポーネントをステートレスにするパターンです。

詳細