これまでの基本編のサンプルは、「基本編のベースとなるプロジェクト」で説明したAppScreen()
関数に、簡単なUIコンポーネントのコードを直接記述してきました。実際のアプリのUIはもっと複雑なので、ソースコードも階層化して作成すべきです。適切に階層化することで、状態に応じてUIを変化させる場合の処理も記述しやすくなります。今回は、UIを階層化し、Kotlinの制御構文を使ってよりシンプルかつ柔軟にUIを記述する方法を説明します。
UIを階層化する
複雑なUIを作成する場合は、コンポーザブル関数から別のコンポーザブル関数を呼び出すことによって、UIを階層化することができます。一つのコンポーザブル関数が大きくなりすぎると管理しづらくなりますので、UIのまとまり毎にコンポーザブル関数を定義して、一つ一つのコンポーザブル関数が大きくなりすぎないように保つことが望ましいです。
コンポーザブル関数はKotlinの関数と同じように扱えるので、ソースコードを書いている途中でいつでも、処理の一部を別の関数に分割することができます。従来のXMLで記述するViewの場合、一部分を別のXMLに分離しようとするとそれなりに手間でしたので、UI設計時にどの範囲を一つのXMLで書くかを決めておくことが多かったと思います。あるいは、一つのフラグメントには一つのXMLと決め打ちということも多かったと思います。Jetpack Composeでは関数の分離が簡単なので、必要に応じて関数を分離したり統合したりして、一つ一つの関数はシンプルな状態にしておくことができます。
例えば図のようなUIを作るとします。
Image2つ、Text3つ、Button1つを配置しているこのUIを一つのコンポーザブル関数で書くと、次のようになります。コメントを書いていないせいもありますが、お世辞にも見やすいとは言えませんね。
@Composable
fun AppScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
Column(
modifier = Modifier
.weight(1f)
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.dog),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentScale = ContentScale.Crop
)
Text(
text = "Dog",
fontSize = 20.sp,
)
}
Column(
modifier = Modifier
.weight(1f)
.padding(10.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = R.drawable.cat),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentScale = ContentScale.Crop
)
Text(
text = "Cat",
fontSize = 20.sp,
)
}
}
Text(
text = "Do you like animals?",
fontSize = 25.sp,
modifier = Modifier.padding(vertical = 10.dp)
)
Button(
onClick = { /*TODO*/ },
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
) {
Text(text = "Yes")
}
}
}
ではこのコンポーザブル関数を分割して階層化し、UIの構造を分かりやすくしましょう。上記のUIは、このような構造になってます。上部には動物の情報を表示するエリアが2つ並んでいます。表示内容は犬と猫で異なりますが、UIの構造は同じなので、関数化して再利用することができそうです。下部には質問と回答用ボタンを表示するエリアがあります。
AppScreen()
関数は次のようになります。最上位にColumn()
を配置し、全体を上下の2段に分割しています。上部はさらにRow()
で左右に分割し、AnimalComposable()
を2つ配置しています。下段にはQuestionComposable()
を配置しています。Column
とRow
の組み合わせ方は最初のサンプルと同じですが、今回は全体の構造の見通しがとてもよくなりました。
@Composable
fun AppScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
AnimalComposable(
resourceId = R.drawable.dog,
text = "Dog",
modifier = Modifier
.weight(1f)
.padding(10.dp)
)
AnimalComposable(
resourceId = R.drawable.cat,
text = "Cat",
modifier = Modifier
.weight(1f)
.padding(10.dp)
)
}
QuestionComposable()
}
}
AnimalComposable()
とQuestionComposable()
の実装は以下の通りで、基本的には最初のサンプルの該当部分を切り出しただけです。それでも、適切に関数を分割することで見通しがよくなっていることが実感できると思います。
AnimalComposable()
には引数を定義していることもポイントです。コンポーザブル関数も通常のKotlinの関数と同じように、引数を定義することができます。これにより同じ構造のUIを部品として再利用できます。今回の例では画像のリソースIDと表示文字列を呼び出し側から指定することによって、共通で使える動物情報表示のUI部品を作っています。第三引数はModifier
です。呼び出し元では、Column()
やRow()
のスコープの中だけで使えるModifier.weight()
を設定しています。これをAnimalComposable()
のルート階層のColumn()
にそのまま設定しています。このようにすることで、AnimalComposable()
がRow()
の中で正しいサイズと位置に表示されるようにしています。
@Composable
fun AnimalComposable(resourceId: Int, text: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = resourceId),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentScale = ContentScale.Crop
)
Text(
text = text,
fontSize = 20.sp,
)
}
}
@Composable
fun QuestionComposable() {
Text(
text = "Do you like animals?",
fontSize = 25.sp,
modifier = Modifier.padding(vertical = 10.dp)
)
Button(
onClick = { /*TODO*/ },
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
) {
Text(text = "Yes")
}
}
イベントを上位に返す
UIを階層化すると、下位レイヤーで発生したイベントを上位レイヤーに返したい場合が出てきます。そのような場合は、コンポーザブル関数の引数を使ってイベントハンドラを実装します。例えば上記のQuestionComposable
のボタンクリックイベントを、上位階層のAppScreen
に返したい場合は、下記のような実装になります。
@Composable
fun QuestionComposable(onClick: ()->Unit = {}) {
Text(
...
)
Button(
onClick = onClick,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp)
) {
Text(text = "Yes")
}
}
QuestionComposable()
の引数にonClick
を定義し、Button
のonClick
にそのまま渡します。呼び出し元のAppScreen()
でイベントを受け取るには、次のようにラムダ式を追加します。
@Composable
fun AppScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row {
...
}
QuestionComposable {
/* Button onClick Handler */
}
}
}
なお、QuestionComposable()
のonClick
引数はデフォルト値に{ }
(何も処理をしない空っぽのラムダ式)を指定しているので、QuestionComposable()
の呼び出し元で何も指定しなくてもエラーにはなりません。また、上位階層に渡すイベントは必ずしもオリジナルのイベントと同じでなくても構いません。例えばQuestionComposable
にボタンが2つある場合、ボタンごとにIDを割り当て、上位に返すイベントの引数にIDを渡すようにするといったことも考えられます。
コンポーザブル関数内で制御構文を使う
繰り返しになりますが、コンポーザブル関数は基本的に通常のKotlinの関数と同じことができます。したがって、if
やfor
などの制御構文も使えます。
if文で表示を切り替える
コンポーネントの引数の変更ではカバーしきれない大きな変更を実装するには、if
やwhen
による分岐を活用します。例えば、コンポーネントの表示・非表示を切り替える場合、従来のView
システムではvisible
パラメータを使ったり、Flagment
を追加・削除したりしていました。Jetpack Composeではif
文などで表示すべきかどうかの条件を判定し、条件を満たす場合にコンポーザブル関数を呼び出します。
上のサンプルで、ボタンが押されたら応答を表示するようにしてみましょう。
@Composable
fun AppScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
var isButtonPressed by remember { mutableStateOf(false) }
Row {
...
}
QuestionComposable {
isButtonPressed = true
}
if (isButtonPressed) {
ResponseComposable()
}
}
}
@Composable
fun ResponseComposable() {
Text(
text = "Thank you!!!",
fontSize = 25.sp
)
}
まずボタンが押されたかどうかの状態を保持する変数isButtonPressed
をremember
で定義します。初期値はfalse
です。remember
やmutableStateOf()
についての説明は、「ボタンクリックでUIを更新する」をご覧ください。
そして、QuestionComposable()
のonClick
引数(ラムダ式の部分)で、isButtonPressed
をtrue
にします。
ResponseComposable()
は、isButtonPressed
がtrue
の場合に呼び出されます。isButtonPressed
の初期値はfalse
ですので、最初にUIを描画する時点ではResponseComposable()
は描画されません。その後QuestionComposable()
内のボタンがクリックされると、onClick
イベントが呼び出され、isButtonPressed
がtrue
に変化します。Jetpack Composeはこの変化を検出し、UIを再構築します。そして再度AppScreen()
が呼び出されたときには、isButtonPressed
はtrue
になっているので、ResponseComposable
が表示されます。
for文で同じUIを並べる
同じUIのブロックを複数並べる場合には、for
文が使えます。例えば上の例では2つのAnimalComposable
をならべて記述していますが、これをfor
文に置き換えることもできます。せっかくなので3列に増やしてfor
文で書いてみます。
fun AppScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
val animalList = listOf<Animal>(
Animal("Dog", R.drawable.dog),
Animal("Cat", R.drawable.cat),
Animal("Bird", R.drawable.bird),
)
Row {
for (animal in animalList) {
AnimalComposable(
resourceId = animal.resourceId,
text = animal.text,
modifier = Modifier
.weight(1f)
.padding(10.dp)
)
}
}
...
}
}
ただし気を付けてほしいのは、このようなfor
文の使い方をするのは、繰り返しの数が固定で、それほど多くない場合に限られます。長いリストを表示するような場合は、実践編で説明するLazyColumn
やLazyRow
を使います。
まとめ
今回は、UIを階層化し、制御構文を活用したコンポーザブル関数の記述も導入して、より柔軟にUIを記述する方法を学びました。動的なUIを作成する際には、UIの記述中にif
文やfor
文などを使えるのはとても便利です。ただし気を付けてほしいのは、UIのソースコード内にビジネスロジックを書いてしまうと、メンテナンスしづらいコードになってしまいます。コンポーザブル関数内ではあくまでUIの条件判定などにとどめ、ビジネスロジックはModel(あるいはViewModel)に記述しましょう。コンポーザブル関数とViewModelの結び付け方は、実践編で説明するつもりです。
Jetpack Compose入門