Skip to content

Jetpack Compose

Jetpack Compose is a system for building user interfaces on Android. It’s a modern replacement for the older “Views” system, which used XML to define the layout and Java hooks to add interactivity and populate the UI content. It’s Android’s answer to SwiftUI.

Here is how it gets its name:

  • Jetpack: Android’s modern suite of libraries that replace older libraries. Jetpack libraries are better suited for app development today because you ship them with your app, meaning you can update them without requiring the user to update their device, and they have built-in support for a wide range of Android versions. Jetpack libraries are distributed under the androidx.* package name.
  • Compose: a paradigm where UI elements are “composable”, meaning your user interface is a pure function of your program state, and it’s represented as a tree of UI components that can contain other UI components. If you’ve ever used React, Jetpack Compose is pretty similar in principle.

Every composable is just a function. It can call other composable functions to add them to the UI.

When you created your Android Studio project, the IDE should have opened a file called MainActivity.kt for you. At the end, it has this code:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
EmptyAppTheme {
Greeting("Android")
}
}

Notice that every function here is marked with the @Composable annotation. This annotation tells the compiler that the function emits UI and opts it into the Compose runtime. As a result, a composable can only be called from another composable. You can’t call Greeting or Text from ordinary Kotlin code. Every piece of your UI has to be reached through a composable root like GreetingPreview.

By convention, composable functions are named in PascalCase, just like classes. This signals that the function describes a piece of UI, so you read it like a component rather than an action.

Here’s the same UI drawn as the tree that Compose builds. Each level is a composable that the level above it called:

EmptyAppTheme
└── Greeting(name = "Android")
└── Text(text = "Hello Android!")

Because composables are just functions, this tree is built simply by calling one composable from another. When Greeting runs, it calls Text, which makes Text its child. This is what we mean by “composition.”

Identify all of the composables in this snippet.

Answer (click to expand)
  1. Greeting
  2. Text
  3. GreetingPreview
  4. EmptyAppTheme

In Jetpack Compose, the user interface (UI) is a pure function of the app’s state. In this context, “pure” means side-effect-free. You never modify the contents of the UI directly. Instead, you change the state, which causes the affected parts of the UI to rerender.

Why do we do this? Well, consider the old way of doing things. Every time we updated our state, we had to update the UI in every place that we knew needed to change. For example, updating the count displayed on a label:

count++ // Update the count
val label = findViewById<TextView>(R.id.counterLabel) // Find where the count is displayed
label.text = "Count: $count" // Manually update the label to reflect the new count

With Jetpack Compose, you keep track of state like a variable:

@Composable
fun Counter() {
val count by remember { mutableStateOf(0) }
Text("Count: $count")
}

In this code snippet, count is a delegated property, which means Compose can run some extra code every time you get or set the variable. This means you get the Compose “magic” without adding anything special to your code; just use the variable normally.

Every time we change the value of count, the content of the label gets updated automatically through a process called recomposition, where the Compose runtime looks at the changed state, finds all the composables that use that state, and executes them again to produce a new user interface.

Compose decides when to rerun your composables, and it may run them many times and in any order. That means you need to be careful about the kinds of logic you put in them: composables need to be pure, which means they can’t have any side effects. A side effect, like a network request, a log statement, or mutating some variable, would run again on every recomposition, even when nothing visible to the user has changed. Composables should only describe the UI for the current state. Anything that actually does something belongs elsewhere.

Implement a counter component. Use the built-in Button and Text composables to display the current count on a button. The count should start at 0. When the button is pressed, the count should increase.

Solution (click to expand)
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}

Implement a Nested composable that can be used like this:

Nested {
Text("Hello, world!")
}

It should display the content inside the block verbatim. Remember that these blocks after a function name are syntactic sugar for including it as the last parameter:

Nested({
Text("Hello, world!")
})
Solution (click to expand)

Composables are just functions, so you can accept one as a parameter and call it to add it to the UI. Lots of built-in composables follow this pattern to give you a layout while letting you specify the content that goes inside that layout.

@Composable
fun Nested(Inner: @Composable () -> Unit) {
Inner()
}