Getting started with Jetpack Compose

Chathil
bhinneka-tech
Published in
8 min readOct 16, 2022

A code to show and hide the view programmatically on Android using XML looks something like this:

expandedView.visibility = if (isExpanded) View.VISIBLE else View.GONE

Let’s say initially the value of isExpandedis false then it changes to true after some user interaction. We need to have a code to handle the view from View.GONE to View.VISIBLE, and also revert them back to View.GONE in case the previous conditions of the said view is View.VISIBLE. This is because the XML UI tree is mutable and we are manually manipulating it based on the previous state of the UI tree. In other words, we are picking up where we left off. If we forget to handle one of the cases, the view will keep the previous state for the lifetime of the activity or fragment. This is just a very basic case, imagine there are much more views that are affected by isExpanded value change. During the lifetime of an Activity or a Fragment, some other data might also change and it’s easy to forget to update views to reflect the latest data.

Jetpack Compose will help us solve this problem and much more, it’s a declarative UI toolkit for Android. Every time the data source change, the entire UI that depends on that data will be regenerated from scratch and then we can apply the necessary update. Back to our case above, because the view will reset every time there’s a data change, expandedView visibility will just go back to View.GONE. What’s left for us to handle is if isExpanded=true.

if (isExpanded) expandedView.visibility = View.VISIBLE

But the piece of code above is not how we write code in Jetpack Compose. In this post, we will learn how to create UI with Jetpack Compose and update them when the data changes. Before we start though, let’s get familiar with the words that will be used in this post:

  1. State: A broad definition of State is a value that can change over time. In Jetpack Compose, we observe this value and react to its change.
  2. Composable: A function that describes the UI. It’s similar to View in XML. TextView in XML is equivalent to Text Composable in Jetpack Compose.
  3. Recomposition: An operation that takes place when the State changes and Jetpack Compose re-generate the Composable.
  4. Side Effects: According to the official Jetpack Compose documentation, A side-effect is a change to the state of the app that happens outside the scope of a composable function. So when an app request data from a server and then get a response that causes the state to change, that state change is the side-effect of the request.

Entry to Composable Function

If we create a new project in the latest Android Studio, there’s an option for a Jetpack Compose project called “Empty Compose Activity”. Like any other project created with Android Studio there’s a MainActivity that should look something like this:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePlaygroundTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}
}
}
}
}
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ComposePlaygroundTheme {
Greeting("Android")
}
}

But there’s a little difference. Inside onCreate function, instead of using setContent to set the XML file, there’s a new setContent that takes Composable function. In the code above, directly inside setContent is a theme Composable. The theme name by default follow your project name with the word Theme at the end. We’ll talk about theming at a later time in another post, hopefully.

Defining a Composable Function

In the code above you can see a built-in Composable called Surface, It act as a background for Greeting Composable. Greeting is a Composable that’s defined in this project. To mark a function as a Composable function, we need to add @Composable annotation to that function then we could start constructing the view. In the code above, the only view is Text composable that requires String as a mandatory argument. Here are some things to note about this function:

  • Composable function doesn’t return anything. instead, it just decides what to show based on the state that it observes.
  • Like any other Kotlin function, a composable function can take arguments.
  • Composable without a layout like the Greeting composable above will follow the parent Composable layout. But Greeting doesn’t have any parent composable with layout, so it will just stack on top. To try this out go ahead and add another composable into the Greeting composable, it could be any composable but in this case, we’ll use Icon. We will talk more about layout once we get to the Layout section of this post.
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
Icon(Icons.Default.Face, "a face icon")
}
A Composable without any layout
  • The screenshot above is a Preview. To mark a function a Preview we need to add both @Composable & @Preview annotation, the order doesn’t matter. @Preview annotation also takes argument that you can check out if you hold control and click the annotation. One of the argument is name, if you leave this argument blank it will take the function name a the title of the preview. In this case that is DefaultPreview. Inside this preview composable, you can call the composable that you need to show inside of it. Inside the preview composable it is common to use Theme composable before actually calling the composable to show inside the preview. Otherwise it will loose the theming. If we have composable with arguments, we can make different preview with the same composable but different arguments to see how it would look across different state. If we click the first icon from the right, it will launch just this preview into the emulator and we can interact with it if it’s clickable or any other forms of user input. We could also interact directly inside Android Studio by clicking the second icon from the right.

Modifying a Composable Function

All built-in composable take a Modifier as one of their arguments. A modifier is used to decorate or enhance composable. For example, to set a view to match the size of its parent in the XML we would use android:layout_width=“match_parent” & android:layout_height=“match_parent”. With Modifier, we could do something like this:

Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}

Modifiers can be chained and their orders matters, there’s more explanation about this over here.

Make composable scrollable

One of the modifiers that can make composable to be scrollable is .verticalScroll(). To test this out, let’s apply .verticalScroll() to our Surface. verticalScroll() takes rememberScrollState()as it’s required argument. Maybe we will talk more about State, later, in another post.

Surface(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
}

If we run and try to scroll it won’t work just yet, because the content of the Surface doesn’t exceed the screen size of the device. So we need to add another composable to fill the entire screen, like this:

Surface(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
color = MaterialTheme.colorScheme.background
) {
Greeting("Android")
Spacer(modifier = Modifier.height(1200.dp))
}

And now it should work. There’s also .horizontalScroll() to make composable scrollable horizontally.

Warning: Scroll modifier can be applied to any composable that takes Modifier, but make sure to avoid having verticalScroll() inside another verticalScroll(). In other word avoid having multiple scrollable in the same direction. Also don’t apply it to composable like Button or Text, because it doesn’t contain another composable.

Make composable clickable

To make something clickable we could use .clickable() modifier and perform an operation inside the lambda. The code will look like this:

@Composable
fun Greeting(name: String) {
val context = LocalContext.current
Text(
text = "Hello $name!",
modifier = Modifier.clickable {
Toast.makeText(
context,
"Hello $name!",
Toast.LENGTH_SHORT
).show()
}
)
}

Common Layout

There are 3 common layout in Jetpack Compose:

Row

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Row(
modifier = modifier.padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "Hello $name!")
Icon(Icons.Default.Face, "a face icon")
}
}

Row is used to align composable horizontally. Besides Modifier, it takes two other optional arguments. .verticalAlignment() is how to position the view vertically inside it. In this case, we want it to center vertically. verticalAlignment default value is Alignment.Top. The other one is horizontalArrangement, like the name suggests, it arranges the view horizontally. In this case, we want it to be spaced by 8dp, the default value is Arrangement.Start. Here’s the preview of the code above:

Column

@Composable
fun PullDownToRefresh(modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(Icons.Default.Refresh, “refresh icon")
Text(text = "Pull down to refresh", style = MaterialTheme.typography.bodySmall)
}
}

Column is used to align composable vertically. Its arguments are the same as Row, but in a different direction. Here’s the preview of the code above:

Box

@Preview
@Composable
private fun LikedImage() {
Box(
contentAlignment = Alignment.TopEnd
) {
Image(
painter = painterResource(id = R.drawable.placeholder_img),
contentDescription = "image",
modifier = Modifier.size(162.dp)
)
Icon(
imageVector = Icons.Default.ThumbUp,
contentDescription = "Thumb up",
modifier = Modifier.padding(16.dp)
)
}
}

Box is used to stack composable on top of one another. Besides Modifier, it takes 2 other arguments. One of them is contentAlignment, like in the code above. It is used to align content inside the Box. It handles both x & y axis so it’s a little more complex than the Column and Row composable. Here’s the preview from the code above:

The Slot API

Composable function can take another composable function as its arguments, it’s called the Slot API:

@Composable
private fun ButtonWithIcon(
text: String,
modifier: Modifier = Modifier,
icon: @Composable () -> Unit
) {
Row(modifier = modifier) {
icon()
Spacer(modifier = Modifier.width(8.dp))
Text(text)
}
}
// Usage
ButtonWithIcon("Dislike", modifier = Modifier.padding(8.dp)) {
Icon(
imageVector = Icons.Default.ThumbUp,
contentDescription = "Thumb up",
modifier = Modifier.padding(16.dp)
)
}

One of the use cases for this is for a button that takes other composable and helps position them. In the example above we could also change text to a slot. We can also provide Scope and be able to do something like this:

@Composable
fun PulldownContainer(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit // RowScope, BoxScope is also available
) {
Column(
modifier = modifier.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
content = content
)
}
// Usage
@Composable
fun Pulldown() {
PulldownContainer {
Icon(Icons.Default.Refresh, "a face icon")
Text(text = "Pull down to refresh", style = MaterialTheme.typography.bodySmall)
}
}

That’s it for now

So far we’ve only made small individual composable. There’s a lot more that I want to talk about in this post but I personally think it’s already too long to read. In the next post, we will put the piece together and try to make a whole screen.

--

--