Compose API から学ぶ Kotlin 表現
--
この記事は Qiita Android Advent Calendar 2021 21日目の記事です。
Jetpack Compose は Kotlin First を宣言した Google が作り上げた、最も素晴らしいライブラリの一つです。Kotlin の豊かな表現力が Compose でどのように活かされているかを知れば、Kotlin 力が高まるに違いありません。この記事では、Compose の API 群で 利用されている Kotlin 表現のうち、僕が好きなテクニックを紹介していきます。
Arguments
めちゃくちゃ基本ですが、僕は Kotlin の関数やクラスメソッドの引数の書き方が好きです。Kotlin の引数には「デフォルト引数」と「名前付き引数」という機能があります。
デフォルト引数は、関数呼び出し時に引数の値を指定しないで呼び出せる記法です。Text
Composable を例にして便利さを見ていきましょう。
modifier: Modifier = Modifier
のように書くと関数呼び出し時にその引数に値を指定しなくてよくなります。引数指定しなかった場合は 関数の定義側が =
で指定しているデフォルト値が使用されます。
デフォルト引数はText
のように引数がたくさんある場合に便利です。Text
の利用者はText
の引数のうち、自分のコードに関係する引数だけ意識すればよくなります。
標準で用意されているComposableは引数がとても多いです。それでも簡単に書ける理由は、ほとんどの引数にデフォルト引数が設定されているためです。細かい引数を設定せずとも簡単に使えるAPI、必要なときにカスタマイズできるAPI、その両方を両立できるデフォルト引数は強力な機能です。
名前付き引数は引数名を指定して値を渡す呼び出し方です。例えばModifier.padding
を呼び出すときを考えてみます。
Modifier.padding
は UI の上下左右に余白をつける Modifier です。上下左右の余白の値をそれぞれ受け取ります。これを引数名無しで呼び出すと、渡した値がどこの余白として使われるかわかりにくいです。
名前付き引数を使うと、呼び出し側で引数名と値が1:1で対応するため、ひと目で余白の設定がわかります。
名前付き引数を使って呼び出しておくと、チーム開発しているときにとても役に立ちます。コードを書いた人は引数と値の組み合わせが分かっていたとしても、他の人に同じように伝わるとは限らないからです。
Lambdas
Kotlin は関数そのものが型として評価されるため、関数に関数を渡せます。関数に関数を渡すときに出てくる記法を Lambda (ラムダ) といいます。
Lambda はCompose API の様々な場所で登場するとても便利なやつです。
とてもわかり易い例はCheckboxの onCheckedChange
です。onCheckedChange
はCheckboxの値がタップなどで変わったときに呼ばれる関数です。開発者が任意の処理を記述できます。
記述方法の詳細説明はKotlinのドキュメントに譲りますが、一言でいうと {引数 -> 処理内容}
という記法です。
Javaで作られたAndroid Viewでは onCheckedChange
のようなコールバック関数は Listener インターフェースを使った実装が一般的でした。Kotlinでは Lambdaが面倒なインターフェース実装を省略して簡潔な記述を提供してくれます。(JavaもLambdaはありますが)
このように、Lambdaを使うと関数に関数を渡すとき、渡したい関数の処理をインラインで定義できます。このインラインで関数を定義できる性質はとても強力で、Composeの宣言的な記法を支えていると言っても過言では無いでしょう。今度はButtonを例にして説明していきます。
Buttonは引数の1つに content: @Composable RowScope.() -> Unit
という関数を取ります。これはButtonの中のレイアウトを記述するためのComposable 関数です。ここではシンプルにButtonのテキストを表示しましょう。
Buttonにわたす content
関数の中にTextが記述されています。この階層関係は実際にレンダリングされたUIの階層関係と一致します。
つまり、Lambdaで子のUIに相当するComposable 関数を渡すAPI設計であるため、実際にレンダリングされるUIとコードの階層関係が一致しており、より直感的にUIのコードを書けるということです。
ちなみにcontent
はButton関数の最後の引数なので、Trailing lambdaという記法を使って渡しています。Trailing lambda を使うとlambdaを()の外に書けるため、より宣言的でシンプルな見た目になります。複数の引数の中にlambdaで渡す関数がある場合は、最後の引数にしておくと Trailing lambda で書けるため、呼び出し側がわかりやすくなるでしょう。
Scope
前述したButtonの引数 content
の型が @Composable RowScope.() -> Unit
であるのを完全にスルーしていました。実はこれも素晴らしい仕組みなので紹介します。
RowScope
をレシーバとする関数の中では、UIを横並びにレイアウトする Row
用の Modifier が使えます。例えば、Modifier.align(alignment: Alignment.Vertical): Modifier
やModifier.alignBy(alignmentLine: HorizontalAlignmentLine): Modifier
などです。これらは Row
内でしか呼び出せず、 Row
の外で呼ぶとコンパイルエラーとなります。Button
はボタン内部のレイアウトを横並びにするために Row
を使っており、内部のコンテンツを @Composable RowScope.() -> Unit
で受け付けます。
このように特定の関数内でしか使えない機能を用意する仕組みが Scope
です。
Scopeの実現方法はシンプルです。
上記の例はScopeを実現する最小限のコードです。基本的には MyScope
のようにScopeを表現したクラスを用意します。そして、拡張関数などを使ってMyScopeを使わせたい関数の Reciever を MyScope にします。これをModifier の仕組みに応用する場合は以下のようになります。
Modifier はグローバルで使えるオブジェクトです。MyScope内だけで使えるModifier のメソッドを作りたい場合は、MyScope 内にModifierの拡張関数を定義します。あとは最初のサンプルと同じように MyScope を Reciever に持つ関数を作ってやれば、特定の関数内でしか使えない Modifier.scopeMethod()
の出来上がりです。
このScopeというRecieverを使ったメソッド可視性制御は他にもたくさん応用できます。例えば、変数の変更を許可するScopeを作って internal でのみ公開すれば、データ変更のルールがより型安全できたりします。Kotlin のAPIには Reciever を使ったテクニックがたくさん登場するので、使いこなすとより便利なAPIを自作できます。
余談ですが、Kotlin では現在 Multiple recievers というものが検討されています。今後 Reciever 周りは更に改良がされていくみたいなので、興味があればそちらも追ってみてはどうでしょうか。
https://youtrack.jetbrains.com/issue/KT-42435
https://github.com/Kotlin/KEEP/blob/context-receivers/proposals/context-receivers.md
まとめ
Composeでは本記事で紹介したテクニック以外にもたくさんのKotlin表現が使われています。Composeの実装を見てみるときはKotlinの書き方にも注目してみると、一層楽しいAndroid開発が過ごせるでしょう!