概要
Jetpack Compose を使う上で基本となる、 View の状態を表す変数の宣言方法とそれを使った View の更新方法について説明します。
詳しい仕組みについては他の方が書かれていますので、あくまで個人的な備忘録として残したいと思います。
環境
Android Studio version: Android Studio Dolphin 2021.3.1 Patch 1
kotlin version: 1.7.10
compose version: 1.3.1
内容
Jetpack Compose を使って作成した View は Compose によってその状態を常に監視されており、状態が変化すると変化した部分を再描画する処理が行われます。
これを再コンポーズといい、Jetpack Compose で作成した View を更新する手段となっています。
View の状態監視には State
型または MutableState
型の変数が用いられます。
この変数の宣言方法は大きく分けて3パターン存在し、定義方法に応じて View を更新する処理の書き方も異なります。
これらのパターンについて、ボタンをクリックしてテキストを更新するサンプルを用いて説明します。
パターン1: 「=」で変数を宣言
@Composable
fun Greeting() {
// 「=」で変数を宣言
val message = remember {
mutableStateOf("")
}
Column() {
Text(text = "Hello ${message.value}")
Button(onClick = {
// コールバック関数から更新
message.value = "world from Android"
}) {
Text("Button1")
}
Button(onClick = {
// オリジナルの関数から更新
changeText(message)
}) {
Text("Button2")
}
}
}
private fun changeText(message: MutableState<String>) {
message.value = "world from Compose"
}
View の状態を表す変数を「=」で宣言するパターンです。
今回は message
という名前の変数を用います。
これは View に表示するテキストの一部です。
ここに mutableStateOf
関数で作成した MutableState<String>
型のデータを代入します。
直接代入せずに remember
関数を用いることで変数の値がメモリに保存され、再コンポーズ時に更新後の値と保存した値との差分があれば新しい値に置き換えられます。
また、View 更新するには変数の value
プロパティを更新します。
message.value = "world from Android"
この更新処理は onClick
などのコールバック関数から行えるほか、そこからさらに自分で定義した関数(上のコードでは changeText
関数)を呼び出して行うこともできます。
Button(onClick = {
// オリジナルの関数から更新
changeText(message)
}) {
...
private fun changeText(message: MutableState<String>) {
message.value = "world from Compose"
}
なお、この関数は Composable
である必要はありません。
パターン2: 「by」で変数を宣言
@Composable
fun Greeting() {
// 「by」で変数を宣言
var message by remember {
mutableStateOf("")
}
Column() {
Text(text = "Hello $message")
Button(onClick = {
// コールバック関数から更新
message = "world from Android"
}) {
Text("Button1")
}
Button(onClick = {
// オリジナルの関数から更新
changeText { msg -> message = msg }
}) {
Text("Button2")
}
}
private fun changeText (onNext: (msg: String) -> Unit) {
onNext("world from compose")
}
by
を用いて message
変数を委譲プロパティとして宣言するパターンです。
この場合 message
変数は MutableState<String>
型ではなく String
型となります。
そのため View を更新する場合は value
プロパティではなく変数自体の値を更新します。
message = "world from Android"
この方法はパターン1と比べて value
プロパティを都度参照する必要がなくコードを簡略化できるというメリットがあります。
オリジナル関数を呼び出して View を更新する場合は、引数に変数の値を更新する処理を入れた関数を渡す必要があります。
Button(onClick = {
// オリジナルの関数から更新
changeText { msg -> message = msg }
}) {
...
private fun changeText (onNext: (msg: String) -> Unit) {
onNext("world from compose")
}
この場合はパターン1と比べて少し複雑になります。
パターン3: 変数を分解宣言
@Composable
fun Greeting() {
// 分解宣言
val (message, setMessage) = remember { mutableStateOf("") }
Column() {
Text(text = "Hello $message")
Button(onClick = {
// コールバック関数から更新
setMessage("world from Android")
}) {
Text("Button1")
}
Button(onClick = {
// オリジナルの関数から更新
changeText { msg -> setMessage(msg) }
}) {
Text("Button2")
}
}
}
private fun changeText (onNext: (msg: String) -> Unit) {
onNext("world from Compose")
}
変数を分解宣言する方法です。
一つ目の変数はゲッター、二つ目の変数はセッターになります。
そのため、変数の値を参照するときは message
をそのまま参照し、変数の値を更新するときは setMessage("world from Android")
のようにして値をセットします。
オリジナルの関数から更新するときは二つ目の変数を用いてパターン2と同じように書きます。
まとめ
ここまで変数宣言の3つのパターンと更新方法について書きました。
公式ドキュメントによると、状況に応じて3つのパターンからわかりやすいものを選んで書けば良いとのことです。
個人的には慣れるまでは直感的に書けるパターン1を使うことが多くなりそうな気がします。
また、最後に変数の値を更新しても View が更新されないNGパターンをいくつかご紹介します。
NGパターン
変数がMutableState型ではない
@Composable
fun Greeting() {
var message = ""
Column() {
Text(text = "Hello $message")
Button(onClick = {
// コールバック関数から更新
message = "world from Android"
}) {
Text("Button1")
}
Button(onClick = {
// オリジナルの関数から更新
changeText { msg -> message = msg }
}) {
Text("Button2")
}
}
}
この場合 message
変数はただの String
型であるため Compose によって監視されず、再コンポーズの際に差分のチェックが行われません。
なお、以下のように remember
関数を用いて定義した場合も同様です。
@Composable
fun Greeting() {
var message = remember { "world" }
Column() {
Text(text = "Hello $message")
Button(onClick = {
// コールバック関数から更新
message = "world from Android"
}) {
Text("Button1")
}
Button(onClick = {
// オリジナルの関数から更新
changeText { msg -> message = msg }
}) {
Text("Button2")
}
}
}
remember 関数を使っていない
@Composable
fun Greeting() {
val message = mutableStateOf("")
Column() {
Text(text = "Hello ${message.value}")
Button(onClick = {
// コールバック関数から更新
message.value = "world from Android"
}) {
Text("Button1")
}
Button(onClick = {
// オリジナルの関数から更新
changeText(message)
}) {
Text("Button2")
}
}
}
変数は MutableState
型ではありますが、remember
関数を使っていないため再コンポーズの際に変数の値が初期化され、View の表示も変わりません。
Composable関数のスコープ内部から呼び出している
@Composable
fun Greeting() {
// 「=」で変数を定義
val message = remember {
mutableStateOf("")
}
Column() {
Text(message = "Hello ${message.value}")
Button(onClick = {
// コールバック関数から更新
message.value = "world from Android"
}) {
Text("Button1")
}
Button(onClick = {
// オリジナルの関数から更新
changeText(message)
}) {
Text("Button2")
}
}
// Composable関数内部から直接呼出し・・・(A)
changeText2(message)
}
private fun changeText(message: MutableState<String>) {
message.value = "world from Compose"
}
private fun changeText2 (message: MutableState<String>) {
message.value = "world from Composable"
}
Composable
関数のスコープ内部から変数を更新しても変数の変更は無視され、View には反映されません。
これは上のコードのように Composable
関数の内部から別の関数を呼び出して更新する場合でも同じです。((A)の部分)
これは、Greeting
関数が呼ばれる→message.value
が更新される→再コンポジションが発生する→再度 Greeting
関数が呼ばれるという無限ループを回避するためのようです。
なお、Button
の onClick
は Composable
関数のスコープ外であるため問題ないようです。
参考
状態と Jetpack Compose | Android Developers
Jetpack Compose State Practices - Qiita