Jetpack Compose入門(11) ボタンクリックでUIを更新する



今回はクリック(タップ)できるボタンを作成し、ボタンのクリックイベントを取得する方法と、イベントを受け取って表示を更新する方法を学んでいきます。

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に設定する初期値です。

では、rememberMutableStateで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関数が呼び出されると、count0で初期化され、Text()にも0が表示されます。countMutableStateオブジェクトなので、Jetpack Composeが値の変更を監視しています。ボタンのonClickイベントでcountの値がインクリメントされ1に変更されると、Jetpack Composeはこれを検出し、UIの再構築を行い、Column関数が再度呼び出されます。このとき、countrememberで宣言されているので、以前の値(=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入門・実践編で説明できればいいなと思っています。

次回は、文字入力欄の作成方法について説明します。