バージョン情報
plugins { kotlin("jvm") version "1.4.20" id("org.jetbrains.compose") version "0.2.0-build132" }
考え方
普通のKotlinの書き方と全く異なります。Kotlin-Nativeで freeze
というものがあるのと同じくらいには違います。Kotlin-NativeやKotlin-JSくらいJetpack Composeは違うものだと考えてください。この認識がないと詰まります。
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/freeze.html
DSL
以下のように書きます。thisのブロックが何なのかわかりやすいように要らない記述があります。
fun main() = Window { MaterialTheme { Column { this is ColumnScope Button( onClick = { } ) { this is RowScope Text(text = "1") } } } } |
UIツリー
ここでColumnの定義を見てみましょう。これは何かの関数ではなく、グローバルな関数として定義されています。
じゃあこれどうやってUIツリーを形成しているんだという疑問が湧いてきます。ここが普通のKotlinでは無いなと思う部分です。
これは @Composable
を付けるとコンパイラ側が色々いじっているようなのでこういう書き方でUIツリーが形成されるようになっています。こういうものだと思っておきましょう。 Composition
と呼ばれるものに保存されているということだけ覚えておきましょう。
@Composable
な関数は @Composable
な関数内でしか呼ぶことができません。
@Composable @OptIn(ExperimentalLayoutNodeApi::class, InternalLayoutApi::class) inline fun Column( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, content: @Composable ColumnScope.() -> Unit ) { /* 省略 */ }
レンダリング時
Columnの更新が必要ならColumnのブロック、Buttonの更新が必要ならButtonのブロックが何度でも呼ばれます。
ここに直接通信処理や重いロジックを書くと大変なことになります。
Viewを更新する
ボタンの値を更新してみましょう。以下のコードは押しても反応がありません。だってプログラム側はbuttonTextの値が変わったことをどうやって知れば良いのでしょうか。
Column { var buttonText = 0 Button( onClick = { buttonText += 1 } ) { Text(text = buttonText.toString()) } }
以下のように MutableState
経由で値を操作することによって、Buttonの更新が必要であることを通知します。
Column { val buttonText = mutableStateOf(0) Button( onClick = { buttonText.value++ } ) { Text(text = buttonText.value.toString()) } }
実は上記コードにも欠陥があります。以下のように Column
部分に buttonText
を使うコードを差し込むと buttonText
が再代入され、値がクリアされます。
Column { val buttonText = mutableStateOf(0) Button( onClick = { buttonText.value++ } ) { Text(text = buttonText.value.toString()) } Text(text = buttonText.value.toString()) }
そこで使用するのがrememberです。これで前回の値を保持、復元してくれます。
Column { val buttonText = remember { mutableStateOf(0) } Button( onClick = { buttonText.value++ } ) { Text(text = buttonText.value.toString()) } Text(text = buttonText.value.toString()) }
rememberのスコープ
Tab等、画面を切り替える時に覚えておくと良いことです。今回はシンプルにするために奇数と偶数でViewを出し分けるだけにしています。
Rowを使っているのは見やすくするためだけにです。
Column { val count = remember { mutableStateOf(0) } Row { Button( onClick = { count.value++ } ) { Text(text = "count up") } Text(text = count.value.toString()) } if (count.value % 2 == 1) { val buttonText = remember { mutableStateOf(0) } Text("Odd") Button( onClick = { buttonText.value++ } ) { Text(text = buttonText.value.toString()) } } else { val buttonText = remember { mutableStateOf(0) } Text("Even") Button( onClick = { buttonText.value++ } ) { Text(text = buttonText.value.toString()) } } }
奇数と偶数の中でカウントアップされたものは奇数と偶数を切り替えるとデータが消失します。GCみたいですね。
以下のように毎回呼ばれる位置に移動することで奇数偶数を切り替えてもデータが保持されます。 remember
を使う位置もとても重要になってきます。
val buttonTextOdd = remember { mutableStateOf(0) } val buttonTextEven = remember { mutableStateOf(0) } if (count.value % 2 == 1) { Text("Odd") Button( onClick = { buttonTextOdd.value++ } ) { Text(text = buttonTextOdd.value.toString()) } } else { Text("Even") Button( onClick = { buttonTextEven.value++ } ) { Text(text = buttonTextEven.value.toString()) } }
おわりに
コンパイラを色々いじって実現される独自のUIツリーの作り方、独自のデータの持ち方がとても独特で、最初はとっつきにくいですが、理解してしまえばスムーズに実装できると思います。