今回はクリック(タップ)できるボタンを作成し、ボタンのクリックイベントを取得する方法と、イベントを受け取って表示を更新する方法を学んでいきます。
Jetpack Compose入門の連載も10回を超えました。前回までで文字列と画像の表示に関しては、装飾やレイアウトの方法も学んで、かなり自由にUIを構成できるようになってきました。しかしここまでの内容は静的な表示だけを扱ってきました。今回からは、ユーザーの操作に反応して画面を更新する方法を学んでいきます。
Buttonコンポーネント
ボタンは、androidx.compose.materialパッケージのButton()
関数を使って記述します。定義は以下の通りです。
@Composable
fun Button(
onClick: (() -> Unit)?,
modifier: Modifier? = Modifier,
enabled: Boolean? = true,
interactionSource: MutableInteractionSource? = remember { MutableInteractionSource() },
elevation: ButtonElevation? = ButtonDefaults.elevation(),
shape: Shape? = MaterialTheme.shapes.small,
border: BorderStroke? = null,
colors: ButtonColors? = ButtonDefaults.buttonColors(),
contentPadding: PaddingValues? = ButtonDefaults.ContentPadding,
content: (@Composable @ExtensionFunctionType RowScope.() -> Unit)?
): Unit
他のコンポーザブル関数と同じようにButton()
にもたくさんの引数が定義されていますが、以下のように書くことで簡単なボタンを実現できます。
Button(
onClick = { Log.d("Button", "onClick") }
) {
Text(text = "Button")
}
Text()
を記述している{ }
の中身は、ボタンの表示を構成するコンポーザブル関数をラムダ式で書いています。この波括弧の中に書いたUIがそのままボタンの見た目になります。ここではシンプルにText()
を使って”Button”という文字を表示しています。他にもImage()
を使って画像を表示したり、Icon()
というコンポーネントを使ってアイコンを表示したりといったことも可能です。
onClick
引数は、ボタンをクリックしたときに呼び出されるコールバック関数です。この例ではログを出力する処理を記述しているので、クリックするたびにログが出ます。
変数を使ってUIを更新する
ログを出すだけでは実用的でないので、今度はボタンを押すたびに表示を更新してみましょう。次の例では、”Count up!”と書かれたボタンをタップするたびに、count
変数をインクリメントし、現在のcount
の値をテキストで表示しています。onClick
引数のラムダ式内で、count
変数をインクリメントしています。Text()
に設定する文字列内にcount
変数を組み込んでいるので、ボタンを押すたびに表示が更新されるというわけです。
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
var count by remember { mutableStateOf(0) }
Text(
text = "Tap count: $count",
modifier = Modifier.padding(20.dp)
)
Button(
onClick = { count++ }
) {
Text(text = "Count up!")
}
}
このサンプルで重要なのは、変数count
の宣言です。by remember { mutableStateOf(初期値) }
の形で宣言しています。count
はローカル変数ですが、コンポーザブル関数内でby remember
によって宣言した変数は、関数呼び出し時に前回の値を記憶しています。mutableStateOf()
は、値の変更を監視することが可能なMutableState
を返します。mutableStateOf()
の引数の0
は、count
に設定する初期値です。
では、remember
とMutableState
でUIの動的更新をする仕組みを簡単に説明します。
Jetpack ComposeのUI更新の仕組み
一度サンプルコードから離れて、Jetpack ComposeにおけるUI更新の仕組みについて説明します。
Jetpack Composeは、「宣言型」UIフレームワークです。宣言型UIでは、宣言時(UI作成時)にUIの状態をすべて決定し、後から変更しません。UIコンポーネントの状態(表示する文字列や色や大きさなど)を後から変更するということはできない仕組みになっているのです。この仕組みのおかげでソースコードがシンプルになります。
では状態を変更せずにどうやって動的なUIを実現するのかというと、UIの更新が必要になるたびにUI全体を一から再構築します。もちろん再構築はフレームワークが自動で行います。Jetpack Composeフレームワークが状態の更新を検出したら、UI全体を構築しなおます。この状態更新の検出に使われるのが、上のサンプルでも使っているMutableState
というわけです。
ところで、状態更新のたびにUIを再構築していたら負荷がとても高いのではないかと心配になるかもしれません。この点は、Jetpack Composeがうまく処理してくれます。UIの再構築自体はUI全体を対象にしますが、実際に描画をやり直すのは、表示に変更がある部分だけです。そのため、描画の負荷は低く抑えられています。
さて、サンプルの解説に戻りましょう。もう一度ソースコードを載せておきます。
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
var count by remember { mutableStateOf(0) }
Text(
text = "Tap count: $count",
modifier = Modifier.padding(20.dp)
)
Button(
onClick = { count++ }
) {
Text(text = "Count up!")
}
}
最初にUIが構築される際、Column
関数が呼び出されると、count
は0
で初期化され、Text()
にも0
が表示されます。count
はMutableState
オブジェクトなので、Jetpack Composeが値の変更を監視しています。ボタンのonClick
イベントでcount
の値がインクリメントされ1
に変更されると、Jetpack Composeはこれを検出し、UIの再構築を行い、Column
関数が再度呼び出されます。このとき、count
はremember
で宣言されているので、以前の値(=1
)を覚えてます。そして、Text()が再描画され、表示が1
に更新されます。
Modifier.clickable
ここまでButton()
コンポーネントを使ってきましたが、実はクリックイベントは任意のUIコンポーネントに設定できます。クリックイベントの取得には、Modifier.clickable()
を使います。次の例では、画像をクリックできるようにしています。
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
var count by remember { mutableStateOf(0)}
Text(
text = "Tap count: $count",
modifier = Modifier.padding(10.dp)
)
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
modifier = Modifier.size(100.dp).clickable { count++ }
)
}
まとめ(と補足)
今回はクリックイベントを取得してUIを更新する方法を説明しました。また、Jetpack ComposeのUI更新の仕組みについても説明しました。MutableState
を使ってJetpack Composeが状態の変化を監視し、必要なUIコンポーネントを再描画していることも説明しました。ただ、実はMutableState
には、Activityの破棄と同時に破棄されてしまうという問題があります。実際のアプリで使うと、例えば画面を回転したときなどに変数の値が初期値に戻ってしまうという問題が出てしまいます。それを防ぐためのrememberSavable
という宣言方法もあるのですが、これを使うよりはViewModelを使った方がよいと個人的には思います。このあたりの話題は奥が深いので、Jetpack Compose入門・実践編で説明できればいいなと思っています。
次回は、文字入力欄の作成方法について説明します。