What is Jetpack Compose?
XML made you invalidate the UI by hand. Compose recomposes only the nodes whose data changed — and that one difference reorders how you build a screen.
Also published on Medium — read it there if you prefer, or to comment and highlight.

Day 2 of 100. What the toolkit actually changes.
When we talk about declarative languages we usually talk about React Native or SwiftUI. On Android we used imperative XML. XML was not performant when we re-rendered UI: data changed, the UI was complex, the user clicked, or we synced network data and now a sub-component needs to be visible — so we had to invalidate the UI, which is a costly action.
Now we have Jetpack Compose. It gives us recomposition, which only updates the tree nodes whose data is different. With XML we thought about adding the UI first; if something changed in our data source, we applied the logic afterwards, and released.

You can imagine how coupled everything was. After adopting Jetpack Compose we focus on the data source, which is the main part of the application.
Jetpack Compose is a declarative language. It lets us write UI components that depend on data — the UI state is entirely up to the data, and as the data changes the UI updates.
Compose gives us a lot of features that make development faster and more testable:
- A tree-structured node implementation that lets you think in 3D
- Data dependency, which gives fixed UI states
- A Compose test API that makes UI tests easy
- Lifecycle awareness
- Compose previews
Thinking in 1D, 2D and 3D
Before implementing any design, do a short analysis so you write an efficient composable.
Look through the design properly and analyse each component like LEGO. If I asked you to split the design and break it into small pieces, what would they be?
First: how many UI components are there, in 1D? For a post card:
- Placeholder image →
Image - Name →
Text - Time →
Text - Post image →
Image
Second: what is their orientation, in 2D? We have the components; now look at how they
sit on the flat plane. Going from the top, can we fit everything in a Column, and how many
nodes does that make?
Column {
Node 1 — placeholder image + name + time
Node 2 — post image
}
Node 1 breaks down further:
Row {
Image
Column {
Text
Text
}
}
Third: what is behind it, in 3D? We have the design on 2D paper; now think about the
background of the components. It can differ between Node 1 and Node 2, but here the parent
Column has the same white background. Put the 2D components on a solid white background
and it matches the design:
Column(
modifier = Modifier.background(color = Color.White)
) {
Row {
Image
Column {
Text
Text
}
}
Image
}
A lot of people think that wrapping the above in a Box and setting the background achieves
the design. That is wrong. Column, Row and Box are empty sheets: whatever size you
give them and whatever colour you set becomes the background, and anything you put on top
does not inherit that colour. It is the same as putting a pen on a table — the pen is a
separate object.
We are also missing the border. So decide who owns what: the background and the border both
belong to the Column.
Column(
modifier = Modifier
.background(color = Color.White)
.border(width = 1.dp, color = Grey, shape = RoundedCornerShape(0.dp))
) {
Row {
Image
Column {
Text
Text
}
}
Image
}
Just by thinking in one, two and three dimensions you can build any complex UI with a minimal tree hierarchy.
Data dependency gives you fixed UI states
Compose is a declarative language, which lets it depend on data. Whatever data source you have, the UI revolves around it: as the data changes, the UI state changes with it.
val showPostImage by rememberSaveable { mutableStateOf(true) }
Column(
modifier = Modifier
.background(color = Color.White)
.border(width = 1.dp, color = Grey, shape = RoundedCornerShape(0.dp))
) {
Row {
Image
Column {
Text
Text
}
}
if (showPostImage) {
Image
}
}
If we later change showPostImage to false — on a click event, or from wherever the post
data comes from downstream — the image becomes visible or not accordingly, and when the data
changes only the nodes that depend on it are recomposed.
That is the power of it: you sync data from different sources and your UI renders accordingly.
The test API
This is the feature I like most. The test API makes it easy to pick a node, act on it and assert against it.
You can access nodes three ways:
- Using a tag
- Using text
- Getting all nodes, filtering, and asserting
I usually use text when I have a details page or unique content on screen, but when the same text appears more than once, use a tag.
@Composable
fun Post() {
Column(
modifier = Modifier
.testTag("Background")
.background(color = Color.White)
.border(width = 1.dp, color = Grey, shape = RoundedCornerShape(0.dp))
) {
Row {
Image
Column {
Text(modifier = Modifier.testTag("Name"))
Text(modifier = Modifier.testTag("Time"))
}
}
if (showPostImage) {
Image
}
}
}
Then the test:
class Test {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun myTest() {
composeTestRule.setContent {
MyAppTheme {
Post()
}
}
composeTestRule.onNodeWithTag("Name").assertIsDisplayed()
composeTestRule.onNodeWithTag("Time").assertIsDisplayed()
}
}
That is all it takes. The official testing documentation goes further, and we will come back to writing better test suites later in the series.
Lifecycle awareness
With fragments and activities we always had to make sure flows, coroutines and LiveData
were subscribed in the right lifecycle so they survived configuration changes, the fragment
back stack and so on.
In Compose we collect flow data bound to the ViewModel's lifecycle:
@Composable
fun Post(postViewModel: PostViewModel = hiltViewModel()) {
val postData by postViewModel
.postData
.collectAsStateWithLifecycle()
// ...
}
Previews
I know the pain of building XML, running the application, seeing everything work, and fighting build times. Running the app on different screen sizes and fixing the UI accordingly is a lot of work — worth it, but only once you know the pain.
Compose has @Preview. Annotating a function with it generates a preview of that composable
at build time, beside your code.
@Preview
@Composable
fun PostPreview() {
Post()
}
@Preview takes a lot of parameters, which lets you generate previews at different screen
sizes too. Now you don't have to wonder how the UI will look on a real device.
That is the basis of Jetpack Compose.
Day 2 of a 100-day series on Jetpack Compose, working through the official documentation in order.