Google is committed to advancing racial equity for Black communities. See how.

Kotlin for Jetpack Compose

Jetpack Compose is built around Kotlin. In some cases, Kotlin provides special idioms that make it easier to write good Compose code. If you think in another programming language and mentally translate that language to Kotlin, you're likely to miss out on some of the strength of Compose, and you might find it difficult to understand idiomatically-written Kotlin code. Gaining more familiarity with Kotlin's style can help you avoid those pitfalls.

Default arguments

When you write a Kotlin function, you can specify default values for function arguments, used if the caller doesn't explicitly pass those values. This feature reduces the need for overloaded functions.

For example, suppose you want to write a function that draws a square. That function might have a single required parameter, side, specifying the length of each side. It might have several optional parameters, like thickness, edgeColor, fill, and so on; if the caller doesn't specify those, the function uses default values. In other languages, you might expect to write several functions:

// 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 ) {...}
// …

In Kotlin, you can write a single function and specify the default values for the arguments:

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

Besides saving you from having to write multiple redundant functions, this feature makes your code much clearer to read. If the caller doesn't specify a value for an argument, that indicates that they're willing to use the default value. In addition, the named parameters make it much easier to see what's going on. If you look at the code and see a function call like this, you might not know what the parameters mean without checking the drawSquare() code:

drawSquare(30, 5, Color.Red);

By contrast, this code is self-documenting:

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

Most Compose libraries use default arguments, and it's a good practice to do the same for the composable functions that you write. This practice makes your composables customizable, but still makes the default behavior simple to invoke. So, for example, you might create a simple text element like this:

Text(text = "Hello, Android!")

That code has the same effect as the following, much more verbose code, in which more of the Text parameters are set explicitly:

Text(text = "Hello, Android!",
    color = Color.Unspecified,
    fontSize = TextUnit.Unspecified,
    letterSpacing = TextUnit.Unspecified,
    overflow = TextOverflow.Clip
)

Not only is the first code snippet much simpler and easier to read, it's also self-documenting. By specifying only the text parameter, you document that for all the other parameters, you want to use the default values. By contrast, the second snippet implies that you want to explicitly set the values for those other parameters, though the values you set happen to be the default values for the function.

Higher-order functions and lambda expressions

Kotlin supports higher-order functions, functions that receive other functions as parameters. Compose builds upon this approach. For example, the Button composable function provides an onClick lambda parameter. The value of that parameter is a function, which the button calls when the user clicks it:

Button( // …
    onClick = myClickFunction)

Higher-order functions pair naturally with lambda expressions, expressions which evaluate to a function. If you only need the function once, you don't have to define it elsewhere to pass it to the higher-order function. Instead, you can just define the function right there with a lambda expression. The previous example assumes that myClickFunction() is defined elsewhere. But if you only use that function here, it's simpler to just define the function inline with a lambda expression:

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

Trailing lambdas

Kotlin offers a special syntax for calling higher-order functions whose last parameter is a lambda. If you want to pass a lambda expression as that parameter, you can use trailing lambda syntax. Instead of putting the lambda expression within the parentheses, you put it afterwards. This is a common situation in Compose, so you need to be familiar with how the code looks.

For example, the last parameter to all layouts, such as the Column() composable function, is children, a function which emits the child UI elements. Suppose you wanted to create a column containing three text elements, and you need to apply some formatting. This code would work, but it's very cumbersome:

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

Because the children parameter is the last one in the function signature, and we're passing its value as a lambda expression, we can pull it out of the parentheses:

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

The two examples have exactly the same meaning. The braces define the lambda expression that is passed to the content parameter.

In fact, if the only parameter you're passing is that trailing lambda—that is, if the final parameter is a lambda, and you aren't passing any other parameters—you can omit the parentheses altogether. So, for example, suppose you didn't need to pass a modifier to the Column. You could write the code like this:

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

This syntax is quite common in Compose, especially for layout elements like Column. The last parameter is a lambda expression defining the element's children, and those children are specified in braces after the function call.

Scopes and receivers

Some methods and properties are only available in a certain scope. The limited scope lets you offer functionality where it's needed and avoid accidentally using that functionality where it isn't appropriate.

Consider an example used in Compose. When you call the Row layout composable, your content lambda is automatically invoked within a RowScope. This enables Row to expose functionality which is only valid within a Row. The example below demonstrates how Row has exposed a row-specific value for the gravity modifier:

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.align(Alignment.CenterVertically)
    )
}

Some APIs accept lambdas which are called in receiver scope. Those lambdas have access to properties and functions that are defined elsewhere, based on the parameter declaration:

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.
        drawRect( /*...*/ )
    }
)

For more information, see function literals with receiver in the Kotlin documentation.

Delegated properties

Kotlin supports delegated properties. These properties are called as if they were fields, but their value is determined dynamically by evaluating an expression. You can recognize these properties by their use of the by syntax:

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

Other code can access the property with code like this:

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

When println() executes, nameGetterFunction() is called to return the value of the string.

These delegated properties are particularly useful when you're working with state-backed properties:

var showDialog by remember { mutableStateOf(false) }

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

Destructuring data classes

If you define a data class, you can easily access the data with a destructuring declaration. For example, suppose you define a Person class:

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

If you have an object of that type, you can access its values with code like this:

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

// ...

val (name, age) = mary

You'll often see that kind of code in Compose functions:

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.

    // ...
}

Data classes provide a lot of other useful functionality. For example, when you define a data class, the compiler automatically defines useful functions like equals() and copy(). You can find more information in the data classes documentation.

Singleton objects

Kotlin makes it easy to declare singletons, classes which always have one and only one instance. These singletons are declared with the object keyword. Compose often makes use of such objects. For example, MaterialTheme is defined as a singleton object; the MaterialTheme.colors, shapes, and typography properties all contain the values for the current theme.