この記事は Chris Arriola による Android Developers - Medium の記事 " Thinking in Compose " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Compose 基本シリーズのこの投稿では、Compose の考え方に迫りたいと思います。Jetpack Compose は宣言的 UI フレームワークです。つまり、デベロッパーは UI を表示する手順ではなく、表示する内容を記述します。
質問がある方は、この投稿か YouTube、または Twitter で #MADCompose を使ってコメントを残してください。10 月 13 日のライブ Q & A セッションで皆さんの質問にお答えします。
この記事の内容は、MAD Skills の動画でもご覧いただけます。
* 日本語字幕は、YouTube の自動字幕機能から日本語を選択してください
ビューを使う場合、現在の画面から望む画面にするために、UI を変更する手順を 1 つ 1 つ記述します。
たとえば、クリックしたボタンの背景色を変えたいとしましょう。ビューを使う場合、まず XML から初期状態の UI を読み込みます。その後、ユーザーがボタンをクリックしたときに、ボタンを探して setColor を呼び出します。望む画面状態になるまで、すべてのユーザーのインタラクションやプロパティに対してこれを繰り返します。
Compose を使う場合、望む状態になるまで個々の UI コンポーネントを変更する(詳しくは後ほど触れますが、これはエラーにつながりやすい操作です)必要はありません。表示する内容をあらかじめ宣言しておけば、画面の更新が必要になったタイミングでそのすべての内容が再宣言されます。
うれしいことに、XML で UI を記述する必要もなくなり、Kotlin の構造をフル活用しながら Kotlin だけで UI を記述できます。
手順ではなく内容を記述して UI を構築できる点が、Compose とビューの主な違いです。Compose の方がはるかに直感的に使えるのはそのためです。この記事では、この 2 つの違いに加えて、アプリ作成の考え方を Compose にどのようにシフトすべきか説明します。
Compose によるアプリ作成の考え方を理解するため、まずはビューでどのように画面を作るかについて確認しましょう。
次に示すのは、Github で公開されている Compose サンプルアプリの 1 つ、Jetsurvey の画面です。この画面には、ユーザーに回答を求める 1 択の質問が表示されています。いずれかを選択すると、次の質問に進むことができます。
説明を簡単にするため、この画面の中の 1 つのコンポーネント、すなわちアンケートの選択肢の 1 つを作る場合について考えます。
選択肢は、1 つのイメージ、何文字かのテキスト、ラジオボタンを水平に並べたものでできています。ビューでは、次のようにして各要素を XML で定義します。
<!-- survey_answer.xml --><LinearLayout android:orientation="horizontal" > <ImageView android:id="@+id/answer_image" ... /> <TextView android:id="@+id/answer_text" ... /> <RadioButton android:id="@+id/answer_radio_button" ... /></LinearLayout>
<!-- survey_answer.xml -->
<LinearLayout android:orientation="horizontal" >
<ImageView android:id="@+id/answer_image" ... />
<TextView android:id="@+id/answer_text" ... />
<RadioButton android:id="@+id/answer_radio_button" ... />
</LinearLayout>
このビューに UI の状態を設定するには、フラグメントかアクティビティで Kotlin か Java の findViewById を使い、それぞれのビューへの参照を取得します。参照を取得できたら、setImage や setText などのセッター関数を呼び出してそれぞれのビューを変更し、望む UI の状態を表示します¹。この手順では、それぞれのビューを個別に変更することで、初期状態の UI から望む UI の状態に更新します。つまり、状態を変更する手順を記述しています。 UI を表示する手順を記述すると書いたのはそのためです。
ユーザーが回答を選択すると(状態変化が発生すると)、選択されたことをビューで視覚的に表現しなければなりません。この例では、ユーザーが質問の回答を選択したときに、[Next] ボタンを有効にする処理も必要です。あるビューが選択されたときの副作用として別のビューを更新したい場合は、ビューにリスナーを設定して影響を受けるビューを明示的に変更します。
ただし、状態が変化したときに手動でビューを更新するという方法は、エラーにつながりやすくなります。ビューを状態に合わせて更新するのを忘れたり、複数の更新が予期しない形で競合したときに正しくない状態になったりする可能性があります。
たとえば、アプリで画面の回転などの構成変更が発生した場合、ユーザーが選択した内容は正しく覚えていたとしても、その過程で [Next] ボタンを再有効化することを忘れてしまうかもしれません。
アプリの生存期間全体にわたって状態の変化に同期することは、ビューを扱う場合に繰り返し遭遇する問題です。この問題の複雑さは、アプリでビューや依存する状態の数が増えるとともに増大していきます。これは解決できない問題ではありませんが、バグの原因になりがちです。
では、同じコンポーネントを Compose で作る場合、どのように考えるかみてみましょう。
Compose でも、先ほどと同じような形で UI を構築できます。つまり、イメージ、テキスト、ラジオボタンを水平に並べたコンテナを配置します。
ただし、XML を書く代わりに、要素を直接 Kotlin で定義します。
// SurveyAnswer.kt@Composablefun SurveyAnswer(answer: Answer) { Row { Image(answer.image) Text(answer.text) RadioButton(false, onClick = { /* … */ }) }}
// SurveyAnswer.kt@Composable
fun SurveyAnswer(answer: Answer) {
Row {
Image(answer.image)
Text(answer.text)
RadioButton(false, onClick = { /* … */ })
}
Compose の UI はすでに Kotlin で宣言されているので、XML と Kotlin を行き来する必要はなくなります。
Compose の UI 要素は関数であり、オブジェクトではありません。つまり、UI 要素は参照できないので、後からメソッドを呼び出して変更するようなことはできません。
その代わり、UI 要素のすべての制御は渡す状態(引数)によって行います。ここでは、単に UI に表示する内容を記述します。findViewById、setImage、setText を呼び出す必要はありません。UI は呼び出す関数の中に簡潔に記述します。
望みどおりの状態で UI を表示するため、回答のプロパティを Image 関数と Text 関数に、そして今のところ、未選択を示す false を RadioButton 関数に渡します。
ここでとても重要なビューとの違いがあります。それは、この回答をタップしても項目が選択されないことです。その理由は、RadioButton に常に false を渡しているからです。これは、ユーザーの操作に関係なく未選択のままにするという意味になります(まもなく修正しますので、心配は無用です)。
ビューとは違い、Compose の RadioButton はユーザーのイベントによって自動的に変化する状態を保持しません。RadioButton の状態は渡す値によって決まります。
先ほど、「手順ではなく内容」と述べたのはそのためです。UI 関数に必要な状態を渡すことで、UI として表示する内容を宣言します。UI の要素が変化した場合、新しい状態を渡して、すべてを再度呼び出します。一方のビューシステムでは、個々のパーツを手動で制御して、望みどおりの新しい状態を実現しなければなりません。
状態が UI を制御するなら、どうすれば状態を更新して UI を更新できるのでしょうか。Compose では、イベントを通じてこれを実現します。ユーザーが UI 要素を操作すると、UI は onClick などのイベントを発行します。すると、イベント ハンドラが UI の状態を更新すべきかどうかを決定します。
UI の状態が変化すると、その状態に依存する関数(UI 要素)が再実行されます。状態が変化したときに UI が再生成される処理を、再コンポーズと呼びます。状態を UI に変換する処理と、UI を再生成する状態の変化は、Compose が UI フレームワークとして動作する仕組みの中核です。
先ほどの SurveyAnswer コンポーザブルの RadioButton は、操作しても未選択のままでした。この実装を更新して、タップしたときに選択状態が切り替わるようにしてみましょう
まず、selected という boolean 型の変数を定義し、それを RadioButton 関数の引数として渡します。さらに、onClick イベント ハンドラで selected 変数の値を現在の値から逆転させます。こうすることで、クリックしたときに選択状態が切り替わります。この変更を終えると、RadioButton をクリックして選択状態を切り替えられるようになります。
// SurveyAnswer.kt@Composablefun SurveyAnswer(answer: Answer) { Row { /* ... */ var selected: Boolean = // ... RadioButton(selected, onClick = { selected = !selected }) }}
/* ... */
var selected: Boolean = // ...
RadioButton(selected, onClick = {
selected = !selected
})
なお、実際のアプリでは、RadioButton の状態は SurveyAnswer に持たせるのではなく、Answer オブジェクトから取得します。この例は、RadioButton の状態の仕組みを説明することのみを目的としています。
また、selected 変数を実装していない点にも注意してください。これを動作させるには特殊な状態オブジェクトが必要ですが、この点は次の記事で説明します。
以上の内容をまとめます。Compose の考え方は次のとおりです。
ここでは、Compose の考え方をとても大まかに説明しましたが、学ぶべきことはまだまだたくさんあります。
たとえば、Kotlin の関数はどのように動作するのか、状態とはどのようなものか、Compose では他にどのようなコンポーネントが提供されるのかといったことです。こういった内容は、今後のエピソードで説明します。
それまで待てない方は、以下のリソースを確認してみてください。
質問がある方は、下にコメントを記入するか、Twitter でハッシュタグ #MADCompose をお使いください。10 月 13 日に予定されている本シリーズのライブ Q & A でお答えします。お楽しみに!
¹ データ バインディング ライブラリもビューで宣言的にコードを記述する方法の 1 つです。この仕組みは本記事で説明した状態同期問題を回避するために役立つ可能性がありますが、Compose を使うと XML を扱う必要は完全になくなります。
Reviewed by Mari Kawanishi - Developer Marketing Manager, Google Play