この記事は Chris Arriola による Android Developers - Medium の記事 " Composable Functions " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
前回の MAD Skills の Compose の基本に関する記事では、UI を Kotlin で関数として記述するという Compose の考え方について説明しました。もう XML は必要ありません。今回の記事では、この関数をさらに掘り下げ、それを使ってどのように UI を作るのか説明します。
リマインダーですが、10 月 13 日のライブ Q&A セッションで、Compose の基本についての質問にお答えします。質問はこの記事または YouTube にコメントを残すか、#MADCompose ハッシュタグを使って Twitter に投稿してください。( ※ライブ Q&A セッションは終了しました。録画はこちらからご確認いただけます。日本語字幕は、YouTube の自動字幕機能から日本語を選択してください。 )
この関数の動作の仕組みを理解するため、選択式の質問が 1 つある画面の作り方について考えてみます。これは、Compose サンプルに含まれている Jetsurvey の画面です。
この記事の動画版 (英語) はこちらからご覧いただけます。
* 日本語字幕は、YouTube の自動字幕機能から日本語を選択してください
Compose では、このアンケートの回答の 1 つの選択肢は、Image、Text、RadioButton を含む Row 関数として記述できます。
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0@Composablefun SurveyAnswer(answer: Answer) { Row { Image(answer.image) Text(answer.text) RadioButton(false, onClick = { /* … */ }) }}
// Copyright 2022 Google LLC.
// SPDX-License-Identifier: Apache-2.0
@Composable
fun SurveyAnswer(answer: Answer) {
Row {
Image(answer.image)
Text(answer.text)
RadioButton(false, onClick = { /* … */ })
}
Compose で UI コンポーネントを作る場合、関数に @Composable アノテーションをつけます。このアノテーションは、対象の関数がデータを UI に変換する(つまり、選択肢を UI にする)ものであることを Compose コンパイラに伝えます。¹
このアノテーションをつけた関数は、コンポーズ可能な関数、またはコンポーザブルと呼ばれます。Compose では、この関数が UI の構成要素になります。このアノテーションは簡単にすばやく追加できるので、UI を再利用可能な要素のライブラリとして整理しやすくなります。
たとえば、回答候補として提示する一連の選択肢を実装するために、選択肢のリストを受け取る SingleChoiceQuestion という新しい関数を定義し、そこで先ほど定義した SurveyAnswer 関数を呼び出すことができます。
SingleChoiceQuestion
SurveyAnswer
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun SingleChoiceQuestion(answers: List<Answer>) { Column { answers.forEach { answer -> SurveyAnswer(answer = answer) } }}
fun SingleChoiceQuestion(answers: List<Answer>) {
Column {
answers.forEach { answer ->
SurveyAnswer(answer = answer)
SingleChoiceQuestion はパラメータを受け取れるので、それを使ってアプリのロジックを指定できます。この場合は、回答候補のリストを受け取って、そこに含まれる選択肢を UI に表示します。このコンポーザブルは何も返さずに(つまり、`Unit` を返します)UI を生成している点に注意してください。具体的に言えば、生成されるのは Column レイアウト コンポーザブルです。これは Compose ツールキットの一部で、項目を垂直に並べます。この Column の中に、それぞれの選択肢を表す SurveyAnswer コンポーザブルが生成されます。
Column
コンポーザブルは不変です。また、いずれかの選択肢への参照を保持するというようなこと、つまり、コンポーザブルへの参照を保持して、後からその内容を更新することはできません。必要なすべての情報は、コンポーザブルを呼び出すときにパラメータとして渡さなければなりません。
関数は Kotlin で書かれるので、Kotlin の構文と制御フローをフル活用して UI を生成できます。ここでは、forEach で各選択肢の反復処理をし、SurveyAnswer を呼び出して表示しています。条件に基づいて何かを表示したいなら、if 文を使うだけで簡単に実現できます。View.visibility = View.GONE や View.INVISIBLE はもう必要ありません。Compose のような宣言的 UI フレームワークでは、与えられる入力によって UI の表示を変えたい場合、それぞれの入力値に対して UI をどのように表示するかをコンポーザブルに記述しなければなりません。これを実現するには、次のスニペットのような条件文を使います。
forEach
View.visibility = View.GONE
View.INVISIBLE
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun SingleChoiceQuestion(answers: List<Answer>) { Column { if (answers.isEmpty()) { Text("There are no answers to choose from!") } else { answers.forEach { answer -> SurveyAnswer(answer = answer) } } }}
if (answers.isEmpty()) {
Text("There are no answers to choose from!")
} else {
コンポーザブルは、高速かつ副作用がないものでなくてはなりません。同じ引数で複数回呼ばれた場合、同じ動作になる必要があり、プロパティやグローバル変数を変更してはいけません。この特性を持つ関数を、 「べき等」と呼びます。新しい値で関数を再呼び出しするときに、UI が正しく生成されるために、この特性はすべてのコンポーザブルに必須となります
関数に渡すパラメータで UI のすべてが決まる点に注意してください。状態を UI に変換するというのは、このことを表しています。UI が常に同期されることは、関数のロジックによって保証されます。選択肢のリストが変更されれば、関数が再実行されて新しい選択肢のリストから新しい UI が生成され、必要に応じて UI が再描画されます。
状態が変化したときに UI を再生成するこの処理を、再コンポーズと呼びます。コンポーザブルは不変なので、再コンポーズは新しい状態で UI を更新する唯一の仕組みです。
再コンポーズは、コンポーザブルが別の関数パラメータで再度呼び出されたときに発生します。再コンポーズが発生するのは、関数が依存する状態が変わるからです。
たとえば、SurveyAnswer コンポーザブルが isSelected パラメータを受け取るとしましょう。このパラメータは、選択肢が選択されているかどうかを表します。最初は、どの選択肢も選択されていません。
isSelected
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun SingleChoiceQuestion(answers: List<Answer>) { answers.forEach { answer -> SurveyAnswer( answer = answer, isSelected = false, ) }}
SurveyAnswer(
answer = answer,
isSelected = false,
)
ビューの世界では、ビューがそれぞれの状態を保持しているので、いずれかの選択肢の UI 要素をタップすると、その表示が切り替わります。しかし、Compose の世界では、すべての SurveyAnswer コンポーザブルで false が指定されているので、ユーザーが操作したとしても、すべての選択肢が未選択のままになります。ユーザーの操作に視覚的に応答できるようにするには、コンポーザブルを再コンポーズして新しい状態で UI を再生成できるようにしなければなりません。
Compose
そのために、選択されている選択肢を保持する新しい変数を追加します。さらに、この変数は MutableState (英語) でなければなりません。この型は、Compose ランタイムに組み込まれている監視可能な型です。状態が変化すると、それを読み取るすべてのコンポーザブルが自動的に再コンポーズされるようにスケジュールされます。新しい MutableState は、mutableStateOf (英語) メソッドを使って次のように作成します。
MutableState
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun SingleChoiceQuestion(answers: List<Answer>) { // Initialize selectedAnswer to null since no answer will be selected at first var selectedAnswer: MutableState<Answer?> = mutableStateOf(null) answers.forEach { answer -> SurveyAnswer( answer = answer, isSelected = (selectedAnswer.value == answer), ) }}
// Initialize selectedAnswer to null since no answer will be selected at first
var selectedAnswer: MutableState<Answer?> =
mutableStateOf(null)
isSelected = (selectedAnswer.value == answer),
上のスニペットでは、現在選択されている選択肢である selectedAnswer と比較することで、isSelected の値を更新しています。selectedAnswer は MutableState 型なので、選択されている選択肢は value プロパティを使って取得します。この値が変化すると、Compose は自動的に SurveyAnswer を再実行し、それによって選択されている選択がハイライト表示されます。
selectedAnswer
ただし、上のスニペットは正しく動作しません。selectedAnswer 値は、再コンポーズが発生しても保持されなければなりません。そうでないと、SingleChoiceQuestion が再実行されたときにこの値が上書きされてしまいます。これを解決するため、忘れないように mutableStateOf の中で呼び起こす必要があります。これにより、コンポーザブルが再コンポーズされても、値がリセットされずに保持されることが保証されます。構成の変更が発生した場合でも値を保持する別の方法として、rememberSaveable を使うこともできます。
mutableStateOf
rememberSaveable
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun SingleChoiceQuestion(answers: List<Answer>) { var selectedAnswer: MutableState<Answer?> = rememberSaveable { mutableStateOf(null) } answers.forEach { answer -> SurveyAnswer( answer = answer, isSelected = (selectedAnswer.value == answer), ) }}
rememberSaveable { mutableStateOf(null) }
上のコード スニペットは、さらに selectedAnswer 変数で Kotlin の委譲プロパティ構文を使うと、改善できます。こうすることで、型を MutableState<Answer?> から Answer? 値に変えることができます。この構文は、ベースとなる状態の値を直接扱うことができ、MutableState オブジェクトの value プロパティを呼び出す必要がなくなるかなり優れたものです。
MutableState<Answer?>
この新しく追加した状態があれば、onAnswerSelected パラメータにラムダ関数を渡すことで、ユーザーが選択したときにアクションを実行できるようになります。このラムダの定義で、selectedAnswer の値を新しいものに設定します。
onAnswerSelected
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun SingleChoiceQuestion(answers: List<Answer>) { var selectedAnswer: Answer? by rememberSaveable { mutableStateOf(null) } answers.forEach { answer -> SurveyAnswer( answer = answer, isSelected = (selectedAnswer == answer), onAnswerSelected = { answer -> selectedAnswer = answer } ) }}
var selectedAnswer: Answer? by
isSelected = (selectedAnswer == answer),
onAnswerSelected = { answer -> selectedAnswer = answer }
前回の記事を覚えているでしょうか?イベントは状態を更新する仕組みでした。この例では、ユーザーが選択肢をタップしたときに、onAnswerSelected イベントが実行されます。
Compose のランタイムは、状態が読み取られた場所を自動追跡しているので、その状態に依存するコンポーザブルを効率的に再コンポーズできます。そのため、状態を明示的に観測したり、手動で UI を更新したりする必要はなくなります。
コンポーズ可能な関数には、意識しておくべき動作特性がほかにもあります。この動作の特性上、コンポーズ可能な関数は、副作用をもたらさないことに加え、同じ引数で何度呼び出しても同じ動作になることが重要です。
1. コンポーズ可能な関数は任意の順序で実行できる
次のスニペットをご覧ください。このコードは順次実行されると思うかもしれません。しかし、必ずしもそうとは限りません。Compose は、一部の UI 要素の優先度が高いことを認識しているので、そのような要素は最初に描画される可能性があります。たとえば、タブレイアウトに 3 つの画面を描画するコードがあるとしましょう。StartScreen が最初に実行されると思うかもしれませんが、これらの関数はどのような順序でも実行することができます。
StartScreen
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() }}
fun ButtonRow() {
MyFancyNavigation {
StartScreen()
MiddleScreen()
EndScreen()
2. コンポーズ可能な関数は並列して実行できる
コンポーザブルは並列に実行できるので、複数のコアを活用することで画面のレンダリング パフォーマンスが向上します。次のコード スニペットでは、コードは副作用なく実行され、入力リストが UI に変換されます。
ただし、下のスニペットのように、関数からローカル変数に書き込みをする場合、コードは副作用なしとは見なされません。下のコードと同じようなことをすると、UI の動作が不適切になる可能性があります。
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun ListComposable(myList: List<String>) { Row(horizontalArrangement = Arrangement.SpaceBetween) { Column { for (item in myList) { Text("Item: $item") } } Text("Count: ${myList.size}") }}
fun ListComposable(myList: List<String>) {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
for (item in myList) {
Text("Item: $item")
Text("Count: ${myList.size}")
3. 再コンポーズは可能な限りスキップする
Compose は、可能な限り、更新する必要がある UI のみを再コンポーズしようとします。再コンポーズをトリガーした状態を使わないコンポーザブルでは、再コンポーズがスキップされます。次のスニペットで name 文字列が変更された場合、Header コンポーザブルと Footer コンポーザブルはこの状態に依存していないため、再実行されません。
Header
Footer
// Copyright 2022 Google LLC. // SPDX-License-Identifier: Apache-2.0 @Composablefun GreetingScreen(name: String) { Column { Header() Greeting(name = name) Footer() }}
fun GreetingScreen(name: String) {
Header()
Greeting(name = name)
Footer()
4. 再コンポーズは厳密なものではない
再コンポーズは厳密なものではありません。つまり、再コンポーズはパラメータが再度変化する前に終わると想定されます。再コンポーズが終わる前にパラメータが変化した場合、Compose は再コンポーズをキャンセルし、新しいパラメータでもう一度再コンポーズを始める可能性があります。
5. コンポーズ可能な関数は何度も実行されることがある
最後に挙げるのは、コンポーズ可能な関数は何度も実行される可能性があることです。これが問題になるのは、コンポーズ可能な関数に毎フレーム実行する必要があるアニメーションが含まれる場合です。フレームの欠落が起きないようにするために、コンポーズ可能な関数を高速にすることが重要なのはそのためです。
長時間実行オペレーションが必要な場合は、コンポーズ可能な関数で行わないようにしてください。このようなオペレーションは、UI スレッドの外で実行し、コンポーザブルではその結果だけを渡すようにします。
また、コンポーザブルのいくつかの興味深い特性についても学びました。コンポーザブルには次の特性があります。
Compose ツールキットでは、基礎となる強力なコンポーザブルがたくさん提供されています。こういったコンポーザブルは、美しいアプリを作るうえで役立ちます。この点については、次回説明したいと思います。それまで待てない方は、以下のリソースを確認してみてください。
質問がある方は、下にコメントを記入するか、Twitter でハッシュタグ #MADCompose をお使いください。10 月 13 日に予定されている本シリーズのライブ Q&A で質問にお答えします。お楽しみに!( ※ライブ Q&A セッションは終了しました。録画はこちらからご確認いただけます。日本語字幕は、YouTube の自動字幕機能から日本語を選択してください。 )
¹ @Composable アノテーションがついた関数がすべて UI を返すわけではありません。たとえば、remember を呼び出す関数は UI を返しませんが、Compose UI ツリーのノードを生成します。