Google は、黒人コミュニティに対する人種平等の促進に取り組んでいます。取り組みを見る

Jetpack Compose で Kotlin を使用する

Jetpack Compose は Kotlin に基づいて構築されています。場合によっては、優れた Compose コードを簡単に作成できる特別なイディオムが Kotlin によって提供されます。別のプログラミング言語で考えて、その言語を頭の中で Kotlin に変換すると、Compose の強みの一部を見逃してしまうかもしれません。また、Kotlin のイディオムで書かれたコードは理解しづらいこともあるでしょう。Kotlin のスタイルに慣れると、このような落とし穴を避けることができます。

デフォルトの引数

Kotlin 関数を作成するとき、関数の引数のデフォルト値を指定できます。これは、呼び出し元が明示的に値を渡さなかった場合に使用されます。この機能により、関数のオーバーロードの必要性が減ります。

たとえば、正方形を描画する関数を作成するとします。この関数には、各辺の長さを指定する単一の必須パラメータ side があるとします。thicknessedgeColorfill など、省略可能なパラメータが複数あるとします。呼び出し元がこれらを指定しなかった場合、関数はデフォルト値を使用します。他の言語では、複数の関数を作成することになる場合があります。

// We don't need to do this in Kotlin!
void drawSquare(int side) {...}
void drawSquare(int side, int thickness) {...}
void drawSquare(int side, int thickness, Color edgeColor ) {...}
// …

Kotlin では、単一の関数を作成して、引数にデフォルト値を指定できます。

fun drawSquare(
    side: Int, thickness: Int = 2,
    edgeColor: Color = Color.Black,
    fill: Color = Color.White
) { ... }

この機能を使用すると、複数の冗長関数を作成する必要がなくなるだけではなく、コードが非常に読みやすくなります。呼び出し元が引数に値を指定していない場合、これはデフォルト値を使用する意思があることを示しています。さらに、パラメータに名前が付けられているため、状況の把握が容易になります。コードを見ていて次のような関数呼び出しがあった場合、drawSquare() コードを確認しないとパラメータの意味がわかりません。

drawSquare(30, 5, Color.Red);

一方、次のようなコードであれば、コードだけで意味がわかります。

drawSquare(side = 30, thickness = 5, edgeColor = Color.Red)

ほとんどの Compose ライブラリではデフォルトの引数を使用します。作成するコンポーズ可能な関数についても同じようにすることをおすすめします。これにより、コンポーザブルをカスタマイズできるようになりますが、依然としてデフォルトの動作は簡単に呼び出すことができます。そのため、たとえば次のようなシンプルなテキスト要素を作成するとします。

Text(text = "Hello, Android!")

このコードは、次に示すような、より詳細な(より多くの Text パラメータを明示的に設定している)コードと同じ効果をもたらします。

Text(text = "Hello, Android!",
    color = Color.Unset,
    fontSize = TextUnit.Inherit,
    letterSpacing = TextUnit.Inherit,
    Overflow = TextOverflow.Clip)

最初のコード スニペットの方がはるかにシンプルで読みやすいだけでなく、コードだけで意味がわかります。text パラメータのみを指定することで、他のすべてのパラメータに対してデフォルト値を使用することを示せます。一方、2 つ目のスニペットは、他のパラメータの値を明示的に設定することを意味しますが、設定した値は関数のデフォルト値です。

高階関数とラムダ式

Kotlin は、高階関数(他の関数をパラメータとして受け取る関数)をサポートしています。Compose は、このアプローチに基づいています。たとえばコンポーズ可能な関数 Button には、ラムダ パラメータ onClick が用意されています。このパラメータの値は、ユーザーがボタンをクリックしたときに呼び出す関数です。

Button( // …
    onClick = myClickFunction)

高階関数は、ラムダ式(関数に評価される式)と自然に対になります。関数が一度だけ必要な場合は、高階関数に渡すために関数を他の場所で定義する必要はありません。代わりに、ラムダ式を使用して関数をその場で定義できます。上記の例では、myClickFunction() が他の場所で定義されていることを前提としています。関数をここでしか使用しない場合は、ラムダ式を使用して、インラインで関数を定義する方が簡単です。

Button( // ...
    onClick = {
       // do something
       // do something else
    }
)

末尾のラムダ

Kotlin には、最後のパラメータがラムダである高階関数を呼び出すための、特別な構文が用意されています。ラムダ式をパラメータとして渡す場合、末尾のラムダ構文を使用できます。ラムダ式をかっこで囲むのではなく、かっこの後ろに配置します。これは Compose ではよくあることであるため、コードの外観に慣れておく必要があります。

たとえばコンポーズ可能な関数 Column() など、すべてのレイアウトの最後のパラメータは content です。これは、列内の UI 要素を出力する関数です。3 つのテキスト要素を含む列を作成し、書式を適用する必要があるとします。このコードは機能しますが、非常に面倒です。

Column(
    modifier = Modifier.padding(16.dp), content = {
        Text("Some text")
        Text("Some more text")
        Text("Last text")
    }
)

children パラメータは関数署名の最後のパラメータであり、値をラムダ式として渡しているため、かっこ内から値を取り出せます。

Column(modifier = Modifier.padding(16.dp)) {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

この 2 つの例の意味はまったく同じです。中かっこは、content パラメータに渡されるラムダ式を定義しています。

実際には、渡す唯一のパラメータが末尾のラムダである場合(つまり最後のパラメータがラムダであり、他のパラメータは渡さない場合)は、かっこを完全に省略できます。そのため、たとえば Column に修飾子を渡す必要がない場合、次のようなコードを作成できます。

Column {
    Text("Some text")
    Text("Some more text")
    Text("Last text")
}

この構文は、Compose ではごく一般的であり、特に Column のようなレイアウト要素でよく使われます。最後のパラメータは要素の子を定義するラムダ式であり、子は関数呼び出しの後に中かっこで指定します。

スコープとレシーバー

一部のメソッドとプロパティは、特定のスコープでしか利用できません。スコープを制限することで、必要な機能を提供し、適切でない機能を誤って使用しないようにできます。

Compose で使用されている例を考えてみましょう。Row レイアウト コンポーザブルを呼び出すと、コンテンツ ラムダが自動的に RowScope 内で呼び出されます。これにより、RowRow 内でのみ有効な機能を公開できます。下記の例は、Rowgravity 修飾子の行固有の値をどのように公開しているかを示しています。

Row {
    Text(
        text = "Hello world",
        // This Text is inside a RowScope so it has access to
        // Alignment.CenterVertically but not to
        // Alignment.CenterHorizontally, which would be available
        // in a ColumnScope.
        modifier = Modifier.gravity(Alignment.CenterVertically)
    )
}

API によっては、レシーバー スコープで呼び出されるラムダを受け入れるものもあります。こうしたラムダは、パラメータ宣言に基づき、他の場所で定義されているプロパティと関数にアクセスできます。

Box(
    modifier = Modifier.drawBehind {
        // This method accepts a lambda of type DrawScope.() -> Unit
        // therefore in this lambda we can access properties and functions
        // available from DrawScope, such as the `drawRectangle` function.
        drawRectangle(...)
    }
)

詳細については、Kotlin ドキュメントの Function literals with receiver をご覧ください。

委譲プロバティ

Kotlin は、委譲プロパティをサポートしています。こうしたプロパティはあたかもフィールドであるかのように呼び出されますが、その値は、式を評価することで動的に決定されます。こうしたプロパティは、by 構文を使用することで認識できます。

class delegatingClass {
    var name: String by nameGetterFunction()
}

他のコードでは、次のようなコードを使用してプロパティにアクセスできます。

myDC = delegatingClass()
println("The name property is: " + myDC.name)

println() が実行されると、nameGetterFunction() が呼び出され、文字列の値が返されます。

委譲プロパティは、状態に基づくプロパティを扱う場合に特に有用です。

var showDialog by state { false }

// Updating the var automatically triggers a state change
showDialog = true

データクラスの分解

データクラスを定義する場合、分解宣言を使用してデータに簡単にアクセスできます。たとえば、Person クラスを定義するとします。

data class Person(val name: String, val age: Int)

その型のオブジェクトがある場合、次のようなコードを使用してその値にアクセスできます。

val mary = Person(name = "Mary", age = 35)

// ...

val (name, age) = mary

Compose 関数では、そのような種類のコードをよく見かけます。

ConstraintLayout {

    val (image, title, subtitle) = createRefs()

    // The `createRefs` function returns a data object;
    // the first three components are extracted into the
    // image, title, and subtitle variables.

    // ...

}

データクラスには、他にも多くの便利な機能があります。たとえば、データクラスを定義すると、コンパイラは equals()copy() などの便利な関数を自動的に定義します。詳細については、データクラスのドキュメントをご覧ください。

シングルトン オブジェクト

Kotlin を使用すると、シングルトン(インスタンスを常に 1 つだけ持つクラス)を簡単に宣言できます。シングルトンは object キーワードで宣言されます。Compose では、このようなオブジェクトをよく利用します。たとえば、MaterialTheme はシングルトン オブジェクトとして定義されます。MaterialTheme.colorsshapestypography の各プロパティには、現在のテーマの値が含まれています。