1. 始める前に
この Codelab では、状態の概要と、Jetpack Compose での状態の使用方法と操作方法について説明します。
本質的には、アプリの状態とは、時間の経過とともに変化する可能性がある値を指します。この定義は非常に広範であり、データベースからアプリ内の変数まですべてが含まれます。データベースについては後のユニットで詳しく説明しますが、ここで覚えておくべきことは、データベースは構造化された情報(パソコン上のファイルなど)をまとめたものであることです。
すべての Android アプリはユーザーに状態を表示します。Android アプリの状態の例を次にいくつか示します。
- ネットワーク接続を確立できないときに表示されるメッセージ。
- フォーム(登録フォームなど)。状態を記入して送信できます。
- ボタンなどのタップ可能なコントロール。状態は、タップなし、タップ中(ディスプレイ アニメーション)、またはタップ済み(
onClick
アクション)のいずれかです。
この Codelab では、Compose を使用する際の状態の考え方と使い方を学びます。そのために、以下の組み込みの Compose UI 要素を使用して Tip Time というチップ計算アプリをビルドします。
- テキストの入力と編集を行う
TextField
コンポーザブル。 - テキストを表示する
Text
コンポーザブル。 - UI 要素間に空白スペースを表示する
Spacer
コンポーザブル。
この Codelab の最後には、サービス料金を入力するとチップ金額が自動的に計算されるインタラクティブなチップ計算ツールが完成します。以下の画像は最終的なアプリの外観を示しています。
前提条件
- Compose に関する基礎知識(
@Composable
アノテーションなど)。 Row
とColumn
のレイアウト コンポーザブルなど、Compose レイアウトに関する基本的な知識。Modifier.padding()
関数など、修飾子に関する基本的な知識。Text
コンポーザブルに精通していること。
学習内容
- UI で状態について検討する方法。
- Compose が状態を使用してデータを表示する方法。
- テキスト ボックスをアプリに追加する方法。
- 状態をホイスティングする方法。
作成するアプリの概要
- サービス料金に基づいてチップ金額を計算するチップ計算アプリ「Tip Time」。
必要なもの
- ウェブブラウザがインストールされた、インターネットに接続できるパソコン
- Kotlin に関する知識
- Android Studio の最新バージョン
2. 始める
- Google のオンライン チップ計算ツールをご覧ください。これはあくまで一例であり、このコースで作成する Android アプリではありません。
- [Bill] ボックスと [Tip %] ボックスに異なる値を入力します。チップの金額と総額が変わります。
値を入力するとすぐに、[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 リポジトリで確認できます。
スターター アプリの概要
次の手順でスターター コードを確認し、よく理解してください。
- Android Studio でスターター コードのプロジェクトを開きます。
- Android デバイスまたはエミュレータでアプリを実行します。
- 2 つのテキスト コンポーネントが表示されます。1 つはラベル用、もう 1 つはチップ金額を表示するためのものです。
スターター コードのチュートリアル
スターター コードにはテキスト コンポーザブルが含まれています。このパスウェイでは、ユーザーが入力できるようにテキスト フィールドを追加します。作業の土台とするファイルの一部について簡単に説明します。
[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()
}
}
ユーザーの入力を受け取る
このセクションでは、ユーザーがアプリで請求額を入力できる UI 要素を追加します。下の画像のように表示されます。
このアプリはカスタムのスタイルとテーマを使用します。
スタイルとテーマは、単一の UI 要素の外観を指定する属性の集まりです。スタイルでは、アプリ全体に適用できるフォントの色、フォントサイズ、背景色などの属性を指定できます。これらをアプリに実装する方法については、後の Codelab で説明します。現時点では、アプリの外観を美しくするためにすでに対応済みです。
よりわかりやすくするために、カスタムテーマを使用した場合と使用しない場合のアプリのソリューション バージョンを以下に並べます。
カスタムテーマを使用しない場合。 | カスタムテーマを使用した場合。 |
コンポーズ可能な関数 TextField
を使用すると、ユーザーはアプリにテキストを入力できます。たとえば、次の画像に示すように、Gmail アプリのログイン画面にテキスト ボックスが配置されています。
TextField
コンポーザブルをアプリに追加します。
MainActivity.kt
ファイルに、Modifier
パラメータを受け取るコンポーズ可能な関数EditNumberField()
を追加します。TipTimeLayout()
の下のEditNumberField()
関数の本体で、空の文字列に設定されるvalue
名前付きパラメータと、空のラムダ式に設定されるonValueChange
名前付きパラメータを受け入れるTextField
を追加します。
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
- 渡したパラメータに注意してください。
value
パラメータは、ここで渡す文字列値を表示するテキスト ボックスです。onValueChange
パラメータは、ユーザーがテキスト ボックスにテキストを入力するとトリガーされるラムダ コールバックです。
- この関数をインポートします。
import androidx.compose.material3.TextField
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(
...
)
...
}
}
これにより、画面にテキスト ボックスが表示されます。
- [Design] ペインで、
Calculate Tip
テキスト、空のテキスト ボックス、Tip Amount
テキスト コンポーザブルが表示されます。
4. Compose で状態を使用する
アプリにおいて状態とは、時間とともに変化する可能性がある値を指します。このアプリでは、状態は請求額を表します。
状態を保存する変数を追加します。
EditNumberField()
関数の先頭で、val
キーワードを使用してamountInput
変数を追加し、"0"
値に設定します。
val amountInput = "0"
請求額に対するアプリの状態です。
value
名前付きパラメータをamountInput
値に設定します。
TextField(
value = amountInput,
onValueChange = {},
)
- プレビューを確認します。次の画像に示すように、テキスト ボックスに状態変数に設定された値が表示されます。
- エミュレータでアプリを実行し、別の値を入力してください。
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 を更新するための再コンポーズをトリガーします。
サービス料金の状態を追加します。
EditNumberField()
関数で、amountInput
状態変数の前のval
キーワードをvar
キーワードに変更します。
var amountInput = "0"
これにより amountInput
が可変になります。
- 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.
- コンポーズ可能な関数
TextField
で、amountInput.value
プロパティを使用します。
TextField(
value = amountInput.value,
onValueChange = {},
modifier = modifier
)
Compose は、状態 value
プロパティを読み取り、その value
が変更されたときに再コンポーズをトリガーする各コンポーザブルをトラッキングします。
onValueChange
コールバックは、テキスト ボックスの入力が変更されるとトリガーされます。ラムダ式では、it
変数に新しい値が含まれています。
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
変数)を更新します。
- アプリを実行し、テキスト ボックスにテキストを入力します。下の画像で確認できるように、テキスト ボックスには引き続き値
0
が表示されています。
ユーザーがテキスト ボックスにテキストを入力すると、onValueChange
コールバックが呼び出され、amountInput
変数が新しい値で更新されます。amountInput
状態は Compose によってトラッキングされるため、値が変更されると再コンポーズがスケジュール設定され、コンポーズ可能な関数 EditNumberField()
が再度実行されます。そのコンポーズ可能な関数では、amountInput
変数は最初の 0
値にリセットされます。このようにして、テキスト ボックスに 0
値が表示されます。
追加したコードでは、状態の変更によって再コンポーズがスケジュール設定されます。
ただし、EditNumberField()
関数が再コンポーズされるたびに 0
値にリセットされないように、再コンポーズ全体で amountInput
変数の値を保持する必要があります。この問題は次のセクションで解決します。
6. remember 関数を使用して状態を保存する
再コンポーズにより、コンポーズ可能なメソッドが何度も呼び出される場合があります。コンポーザブルは、保存されていない場合、再コンポーズ中に状態をリセットします。
コンポーズ可能な関数は、remember
を使用して再コンポーズ全体でオブジェクトを格納できます。初回コンポーズの際に、remember
関数によって計算された値が Composition に保存され、保存された値は再コンポーズの際に返されます。通常、remember
関数と mutableStateOf
関数はコンポーズ可能な関数で併用し、状態とその更新が UI に適切に反映されるようにします。
EditNumberField()
関数で remember
関数を使用します。
EditNumberField()
関数で、mutableStateOf
()
関数の呼び出しをremember
で囲んで、by
remember
Kotlin プロパティのデリゲートを使用してamountInput
変数を初期化します。mutableStateOf
()
関数で、静的な"0"
文字列ではなく、空の文字列を渡します。
var amountInput by remember { mutableStateOf("") }
これで、空の文字列が amountInput
変数の初期デフォルト値になります。by
は Kotlin のプロパティ委任です。amountInput
プロパティのデフォルトのゲッター関数とセッター関数は、それぞれ remember
クラスのゲッター関数とセッター関数に委任されています。
- 次の関数をインポートします。
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
委任のゲッターとセッターのインポートを追加すると、MutableState
の value
プロパティを参照せずに amountInput
を読み取り、設定できます。
更新された EditNumberField()
関数は次のようになります。
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
var amountInput by remember { mutableStateOf("") }
TextField(
value = amountInput,
onValueChange = { amountInput = it },
modifier = modifier
)
}
- アプリを実行し、テキスト ボックスにテキストを入力します。入力したテキストが表示されます。
7. 状態と再コンポーズの実例
このセクションでは、ブレークポイントを設定してコンポーズ可能な関数 EditNumberField()
をデバッグし、初期コンポーズと再コンポーズの仕組みを確認します。
エミュレータまたはデバイスでブレークポイントを設定し、アプリをデバッグします。
EditNumberField()
関数内の、onValueChange
名前付きパラメータの横にある行ブレークポイントを設定します。- ナビゲーション メニューで、[Debug 'app'] をクリックします。アプリがエミュレータまたはデバイスで起動します。
TextField
要素が最初に作成されたときに、アプリの実行が一時停止します。
- [Debug] ペインで、 [Resume Program] をクリックします。テキスト ボックスが作成されます。
- エミュレータまたはデバイスで、テキスト ボックスに文字を入力します。設定したブレークポイントに到達すると、アプリの実行は再び一時停止します。
テキストを入力すると、onValueChange
コールバックが呼び出されます。ラムダ it
内に、キーパッドで入力した新しい値があります。
「it」の値が amountInput
に割り当てられた後、オブザーバブルな値が変更されると、Compose は新しいデータを使用し再コンポーズをトリガーします。
- [Debug] ペインで、 [Resume Program] をクリックします。エミュレータまたはデバイスで入力したテキストは、この画像に示すように、ブレークポイントがある行の横に表示されます。
これはテキスト フィールドの状態です。
- [Resume Program] をクリックします。入力した値がエミュレータまたはデバイスに表示されます。
8. 外観を変更する
前のセクションでは、テキスト フィールドが機能するように設定しました。このセクションでは、UI を拡張します。
テキスト ボックスにラベルを追加する
すべてのテキスト ボックスに、ユーザーが入力できる情報を示すラベルを設定する必要があります。次のサンプル画像の最初の部分では、ラベルテキストがテキスト フィールドの中央にあり、入力行に沿って表示されています。次のサンプル画像の 2 番目の部分では、ユーザーがテキスト ボックスをクリックしてテキストを入力すると、テキスト ボックス内のラベルが上に移動します。テキスト フィールドの構造について詳しくは、構造をご覧ください。
EditNumberField()
関数を変更して、テキスト フィールドにラベルを追加します。
EditNumberField()
関数のコンポーズ可能な関数TextField()
で、label
名前付きパラメータを追加して空のラムダ式に設定します。
TextField(
//...
label = { }
)
- ラムダ式では、
stringResource
(R.string.
bill_amount
)
を受け入れるText()
関数を呼び出します。
label = { Text(stringResource(R.string.bill_amount)) },
- コンポーズ可能な関数
TextField()
で、singleLine
名前付きパラメータを追加してtrue
値に設定します。
TextField(
// ...
singleLine = true,
)
これにより、テキスト ボックスが複数の行から水平方向にスクロールできる 1 行にまとめられます。
keyboardOptions
パラメータを追加してKeyboardOptions()
に設定します。
import androidx.compose.foundation.text.KeyboardOptions
TextField(
// ...
keyboardOptions = KeyboardOptions(),
)
Android の各種画面では、数字、メールアドレス、URL、パスワードなどの入力用に画面に表示するキーボードを必要に応じて構成できます。他のキーボード タイプについて詳しくは、KeyboardType をご覧ください。
- キーボード タイプを数字キーボードに設定し、数字を入力します。
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
)
}
- アプリを実行します。
キーパッドの変更点を次のスクリーンショットに示します。
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
値を変換する必要があります。
- コンポーズ可能な関数
EditNumberField()
で、amountInput
定義の後にamount
という新しい変数を作成します。amountInput
変数でtoDoubleOrNull
関数を呼び出して、String
をDouble
に変換します。
val amount = amountInput.toDoubleOrNull()
toDoubleOrNull()
関数は、文字列を Double
数値として解析し、結果または null
(文字列が数値の有効な表現でない場合)を返す、事前定義された Kotlin 関数です。
- ステートメントの最後に、
amountInput
が null の場合に0.0
値を返す?:
Elvis 演算子を追加します。
val amount = amountInput.toDoubleOrNull() ?: 0.0
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)
)
}
計算されたチップ金額を表示する
チップ金額を計算する関数を記述したら、次のステップでは計算されたチップ金額を表示します。
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()
関数からはまだ呼び出すことができません。以下の画像は、コードの構造を示しています。
この構造体では、新しい Text
コンポーザブルは amountInput
変数から計算された amount
変数にアクセスする必要があるため、Text
コンポーザブルにチップ金額を表示することはできません。amount
変数を TipTimeLayout()
関数に公開する必要があります。以下の画像は、目的のコード構造を示しています。これにより、EditNumberField()
コンポーザブルはステートレスになります。
このパターンは、状態ホイスティングと呼ばれます。次のセクションでは、コンポーザブルから状態をホイスティング(リフト)して、ステートレスにします。
10. 状態ホイスティング
このセクションでは、コンポーザブルを再利用して共有できるように状態を定義する場所を決定する方法について説明します。
コンポーズ可能な関数で、UI に表示する状態を保持する変数を定義できます。例としては、amountInput
変数を EditNumberField()
コンポーザブルの状態として定義したことが挙げられます。
アプリが複雑になり、他のコンポーザブルが EditNumberField()
コンポーザブル内の状態にアクセスする必要がある場合は、コンポーズ可能な関数 EditNumberField()
から状態をホイスティング(抽出)することを検討する必要があります。
ステートフルとステートレスなコンポーザブルを理解する
次の場合は、状態をホイスティングする必要があります。
- 状態を複数のコンポーズ可能な関数と共有する。
- アプリで再利用できるステートレスなコンポーザブルを作成する。
コンポーズ可能な関数から状態を抽出する場合、作成されるコンポーズ可能な関数はステートレスと呼ばれます。つまり、コンポーズ可能な関数は、それらの状態を抽出してステートレスにできます。
ステートレスなコンポーザブルとは、状態を保持しない(つまり、新しい状態を保持、定義、変更しない)コンポーザブルです。一方、ステートフルなコンポーザブルとは、時間とともに変化する可能性がある状態を所有するコンポーザブルです。
状態ホイスティングは、状態を呼び出し元に移行してコンポーネントをステートレスにするパターンです。
コンポーザブルに状態ホイスティングを適用すると、多くの場合、コンポーザブルに 2 つのパラメータを導入することになります。
value: T
パラメータ。表示する現在の値です。onValueChange: (T) -> Unit
- コールバック ラムダ。値が変更されたときにトリガーされ、ユーザーがテキスト ボックスにテキストを入力したときなど、状態を他の場所で更新できるようにします。
EditNumberField()
関数で状態をホイスティングします。
EditNumberField()
関数定義を更新し、value
パラメータとonValueChange
パラメータを追加して状態をホイスティングします。
@Composable
fun EditNumberField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
//...
value
パラメータは String
型、onValueChange
パラメータは (String) -> Unit
型であるため、String
値を入力として受け取り、戻り値が存在しない関数です。onValueChange
パラメータは、TextField
コンポーザブルに渡される onValueChange
コールバックとして使用されます。
EditNumberField()
関数で、渡されたパラメータを使用するようにコンポーズ可能な関数TextField()
を更新します。
TextField(
value = value,
onValueChange = onValueChange,
// Rest of the code
)
- 状態をホイスティングし、remember で保存した状態を
EditNumberField()
関数からTipTimeLayout()
関数に移動します。
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
//...
) {
//...
}
}
- 状態を
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
)
- 関数
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
プロパティからチップを計算してユーザーに表示できるようになります。
- エミュレータまたはデバイスでアプリを実行し、[Bill Amount] テキスト ボックスに値を入力します。請求額の 15% となるチップ金額が次の画像のように表示されます。
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 を作成することです。
- 再コンポーズは、データが変更されたときに同じコンポーザブルを再度実行してツリーを更新する処理です。
- 状態ホイスティングは、状態を呼び出し元に移行してコンポーネントをステートレスにするパターンです。