Skip to content

Jetpack Compose Concepts

Composable Functions

How does Compose create your user interface?

  1. Composition - composable functions run to declare the user interface in a tree of nodes. (Behind the scenes it uses a Gap buffer, but conceptually it's a tree.)
  2. Layout - compose examines the tree and determines the size of each element and where to place it on the screen.
  3. Drawing - compose renders the elements on the screen for the user to interact with.

Things get interesting when the data passed to a composable function changes. This triggers "recomposition". Compose determines which parts of the UI tree are affected by the changed data, and re-runs the composable functions that created those parts. This updates or replaces parts of the tree. The layout and drawing phases follow, updating the changed parts of the UI.

To make this work efficiently and correctly, you need to follow a few rules:

  • Composable functions must be idempotent. This means that they must always produce the same output when run with the same parameter values. If this isn't the case, Compose would need to refresh the entire UI every time, as it couldn't tell which parts might change.
  • Composable functions should only base their UI declaration on parameters and composition locals (more on that later). They shouldn't reach out to other external objects for data.
  • Composable functions must be free of uncontrolled side effects (we'll talk about controlled side-effects later). They should not directly modify external data or call external functions. Their only communication with the outside world must be through event functions that are passed in.

Composable functions are annotated with @Composable. For example:

@Composable
fun MyList(
    data: List<String>,
    onClick: (String) -> Unit,
) {
    ...
}

This annotation is processed by the Compose Compiler plugin, which runs while the Kotlin compiler is processing your source files.

Behind the scenes, this plugin modifies the function, adding some parameters and setting up code to define a scope for all operations inside the function. The generated code works with the runtime to build the UI-declaration tree, keep track of which state has been read, and invoke the function as part of recomposition. If you're interested in more detail, see Under the hood of Jetpack Compose.

To determine if a composable function needs to be recomposed, a Snapshot tracks the state used by that function. When the snapshot sees new state that doesn't match the last-rendered state, it triggers recomposition.

"Extending" a composable function

Many UI toolkits define classes to represent elements in the UI. You can often create subclasses to define new functionality or set common attributes in a single place for consistency in your application.

Because Compose uses functions to declare the UI, there's nothing to subclass.

To "extend" a composable function, you simply write a new function that calls it, passing in common values for parameters or defining a common structure.

For example, a common pattern in Compose is defining a button:

Button(
    onClick = {
        // what to do when the button is pressed
    },
    modifier = Modifier.padding(8.dp)
) {
    Text(text = text, modifier = Modifier.padding(8.dp))
}

This is very flexible (as you can define whatever you would like inside the button, such as a Row containing an Icon and Text), but a but verbose to write every time you want a button.

You likely have some common attributes you'd like for a button, such as a padding between the border of the button and its contained text, as well as padding outside of the button between it and other components.

We can simplify the above definition with a custom function:

@Composable
fun MyButton(
    text: String,
    onClick: () -> Unit,
) =
Button(
    onClick = onClick,
    modifier = Modifier.padding(8.dp)
) {
    Text(text = text, modifier = Modifier.padding(8.dp))
}

We've pulled in the common code into our own function to create our button, which we can now call using

MyButton("Press Me") {
    // what to do when the button is pressed
}

This locks in the modifier and simplifies the call considerably.