Getting Started with Jetpack Compose: A Modern Approach to Android UI Development

Animesh Roy
13 min readAug 3, 2024

--

Jetpack Compose is a modern toolkit for building Android UI. Compose simplifies and accelerates UI development on Android with less code and intuitive Kotlin capabilities. Using compose you can build UI by defining a set of functions called composable functions that take in data and describe UI elements. Composable functions are the basic building blocks of UI.

We’ll also develop 5 straightforward Android applications by the end of this article.

A Composable function:

  • Describe some part of your UI.
  • It doesn't return anything.
  • Takes some input and generates UI based on that.
  • Composable functions can call other composable functions.
  • Returns nothing and bears the @Composable annotation MUST be named using Pascal case. Pascal's case refers to a naming convention in which the first letter of each word in a compound word is capitalized.

Annotation

To make a function composable you need to annotate it with @composable. Annotations are means of attaching extra information to code. This information helps tools like the Jetpack Compose compiler, and other developers understand the app’s code.

An annotation is applied by prefixing its name (the annotation) with the @ character at the beginning of the declaration you are annotating. Different code elements, including properties, functions, and classes, can be annotated.

The Composable function is annotated with the @Composable annotation. All composable functions must have this annotation. This annotation informs the Compose compiler that this function is intended to convert data into UI. As a reminder, a compiler is a special program that takes the code you wrote, looks at it line by line, and translates it into something the computer can understand (machine language).

@Preview(showBackground = true, name = "Birthday Wishes", showSystemUi = true)
@Composable
fun GreetingPreview() {
Column {
Text(text = "Hello World One")
Text(text = "Hello World Two")
}
}

Jetpack Compose includes a wide range of built-in annotations, which you have already seen @Composable and @Preview annotations so far.

Trailing lambda syntax

In Kotlin, the trailing lambda syntax is a feature that simplifies the way you write code, particularly when working with functions that accept lambda expressions as parameters. This feature is particularly useful in the context of UI frameworks like Jetpack Compose, where it enhances readability and conciseness.

Trailing lambda syntax in Kotlin allows you to move a lambda expression outside of the parentheses of a function call. This makes the code more readable, especially when the lambda is the last argument of the function. It’s a convenient feature when dealing with functions that accept lambda expressions, making the code more expressive and easier to understand.

Here’s a basic example of how trailing lambda syntax works:

fun main() {

// Without trailing lambda syntax
someFunction(“Hello”) { message ->
println(message)
}

// With trailing lambda syntax
someFunction(“Hello”) {
println(it)
}
}

fun someFunction(message: String, action: (String) -> Unit) {
action(message)
}

In the example above, `someFunction` takes a `String` and a lambda function as parameters. By using trailing lambda syntax, the lambda expression `{ println(it) }` is placed outside of the parentheses of the function call, making the function call cleaner and more readable.

In Jetpack Compose, trailing lambda syntax is particularly useful and widely used. Jetpack Compose relies heavily on composable functions, many of which accept lambda expressions to define the content and behavior of UI components. The trailing lambda syntax helps in making composable functions more readable and expressive.

Consider a simple example of a `Column` composable in Jetpack Compose:

import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun MyColumn() {

Column {
Text(text = “First item”)
Text(text = “Second item”)
Text(text = “Third item”)
}
}

@Preview
@Composable
fun PreviewMyColumn() {
MyColumn()
}

In this example, the `Column` composable function takes a lambda as its parameter. By using trailing lambda syntax, you can directly write the content of the `Column` inside the lambda block, making the code more intuitive and aligned with the way UI components are structured.

Benefits of Trailing Lambda Syntax in Jetpack Compose

Enhanced Readability:
By moving the lambda expression outside the parentheses, the code becomes cleaner and more readable. This is particularly valuable in UI development where composable functions can have nested and complex structures.

Natural Flow of UI Components:
Trailing lambda syntax aligns with the natural structure of UI components, where you define the hierarchy and content of the UI in a way that mirrors the actual layout. This makes it easier to visualize and understand the component structure.

Code Organization:
When using trailing lambda syntax, the lambda block is visually separated from the function call, making it easier to see what’s inside the lambda and how it contributes to the overall function.

Here’s a more complex example with nested composables:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun MyComplexUI() {
Column {
Text(text = “Welcome to Jetpack Compose!”)
Row {
Button(onClick = { /* Do something */ }) {
Text(text = “Click Me”)
}
Button(onClick = { /* Do something else */ }) {
Text(text = “Click Me Too”)
}
}
}
}

@Preview
@Composable
fun PreviewMyComplexUI() {
MyComplexUI()
}

In this example, the `Column` and `Row` composables use trailing lambda syntax to define their child composables. This structure makes it clear how the UI components are nested and organized, improving readability and maintainability.

Trailing lambda syntax in Kotlin is a powerful feature that simplifies the syntax and enhances the readability of code, especially when working with functions that accept lambda expressions. In the context of Jetpack Compose, trailing lambda syntax aligns perfectly with the declarative nature of UI development, making it easier to define and understand complex UI structures. By leveraging this syntax, you can write more expressive and maintainable code, leading to a better development experience and more readable UI components. 📱

UI Hierarchy:

Arrange the element in a row or column

A composable function might describe several UI elements. However, if you don’t provide guidance on how to arrange them, Compose might arrange the elements in a way that you don’t like. In this point we will learn how to arrange the composables in a row and in a column.

The UI hierarchy is based on containment, meaning one component can contain one or more components, and the terms parent and child are sometimes used. The context here is that the parent UI elements contain children UI elements, which in turn can contain children UI elements. In this section, we will learn about Column, Row, and Box composables, which can act as parent UI elements.

Row:

@Composable
fun GreetingText(message: String, from: String, modifier: Modifier = Modifier) {
Row {
Text(
text = message,
fontSize = 100.sp,
lineHeight = 116.sp,
)
Text(
text = from,
fontSize = 36.sp
)
}
}

Column:

@Composable
fun GreetingText(message: String, from: String, modifier: Modifier = Modifier) {
Column {
Text(
text = message,
fontSize = 100.sp,
lineHeight = 116.sp,
)
Text(
text = from,
fontSize = 36.sp
)
}
}

Example Happy Birthday App:

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {


GreetingText(
message = "Happy Birthday, Animesh!",
from = "~ From John",
modifier = Modifier.padding(8.dp)

)
}
}
}

@Composable
fun GreetingText(message: String, from: String, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = message,
fontSize = 80.sp,
textAlign = TextAlign.Center,
lineHeight = 100.sp,
)
Text(
text = from,
fontSize = 46.sp,
modifier = Modifier
.padding(16.dp)
.align(Alignment.End)
)
}

}

@Preview(showBackground = true, name = "Birthday Wishes", showSystemUi = true)
@Composable
fun BirthdayCardPreview() {
GreetingText(message = "Happy Birthday Sam!", "From John")
}
Output of the above code

Add an Image composable

@Composable
fun GreetingImage(message: String, from: String, modifier: Modifier = Modifier) {

val image = painterResource(R.drawable.androidparty)

Image(painter = image, contentDescription = "Background image")
}

Add Box Layout

Box layout is one of the standard layout elements in Compose. Use Box layout to stack elements on top of one another. Box layout also lets you configure the specific alignment of the elements that it contains.

@Composable
fun GreetingImage(message: String, from: String, modifier: Modifier = Modifier) {

val image = painterResource(R.drawable.androidparty)

Box() {
Image(painter = image, contentDescription = "Background image")
GreetingText(message = message, from = from, modifier = Modifier.fillMaxSize().padding(8.dp))
}
}

Scale and change the Opacity of the background Image

We have added the image to your app and positioned the image. Now, we need to adjust the scale type of the image, which says how to size the image, to make it fullscreen.

There are quite a few ContentScale types available. We use the ContentScale.Crop parameter scaling, which scales the image uniformly to maintain the aspect ratio so that the width and height of the image are equal to, or larger than, the corresponding dimension of the screen.

Image(
painter = image,
contentDescription = null,
contentScale = ContentScale.Crop
)

To improve the app’s contrast, change the opacity of the background image.

Image(
painter = image,
contentDescription = null,
contentScale = ContentScale.Crop,
alpha = 0.5F
)

Layout Modifiers

Modifiers are used to decorate or add behavior to Jetpack Compose UI elements. For example, you can add backgrounds, padding or behavior to rows, text, or buttons. To set them, a composable or a layout needs to accept a modifier as a parameter. For example:

// Example
Text(
text = "Hello, World!",
// Solid element background color
modifier = Modifier.background(color = Color.Green)
)

Similar to the above example, you can add Modifiers to layouts to position the child elements using arrangement and alignment properties.

To set children’s position within a Row, set the horizontalArrangement and verticalAlignment arguments. For a Column, set the verticalArrangement and horizontalAlignment arguments. The arrangement property is used to arrange the child elements when the size of the layout is larger than the sum of its children.

Below is an illustration of different vertical arrangements:

Below is an illustration of different horizontal arrangements:

Adopt good code practices

When you write apps, it’s important to remember that they may be translated into another language at some point. As you learned in an earlier codelab, a String data type is a sequence of characters, such as "Happy Birthday Animesh!".

A hardcoded string is one that’s written directly in the code of your app. Hardcoded strings make it more difficult to translate your app into other languages and harder to reuse strings in different places in your app. You can extract strings into a resource file to resolve these issues. Instead of hardcoding strings in your code, you put the strings into a file, name the string resources, and use the names whenever you want to use the strings. The name stays the same, even if you change the string or translate it to a different language.

It’s Practice time for what we have learned so far:

1. Compose Article App

package com.appmakerszone.composearticleexercise

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LearnJetpackCompose()
}
}
}

@Composable
fun LearnJetpackCompose() {
Column {

Image(
painter = painterResource(id = R.drawable.bg_compose_background),
contentDescription = "",
modifier = Modifier.fillMaxWidth()
)
Text(
text = "Jetpack Compose tutorial",
fontSize = 24.sp,
modifier = Modifier.padding(16.dp)
)
Text(
text = "Jetpack Compose is a modern toolkit for building native Android UI. Compose simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs.",
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
fontSize = 18.sp,
textAlign = TextAlign.Justify
)
Text(
text = "In this tutorial, you build a simple UI component with declarative functions. You call Compose functions to say what elements you want and the Compose compiler does the rest. Compose is built around Composable functions. These functions let you define your app\\'s UI programmatically because they let you describe how it should look and provide data dependencies, rather than focus on the process of the UI\\'s construction, such as initializing an element and then attaching it to a parent. To create a Composable function, you add the @Composable annotation to the function name.",
modifier = Modifier.padding(16.dp),
fontSize = 18.sp,
textAlign = TextAlign.Justify
)
}

}

@Preview(showBackground = true, showSystemUi = true)
@Composable
fun GreetingPreview() {
LearnJetpackCompose()
}

2. Task Manager App

package com.appmakerszone.composearticleexercise

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AllTaskCompleted()
}
}
}

@Composable
fun AllTaskCompleted() {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(painter = painterResource(id = R.drawable.ic_task_completed),
contentDescription = null,
)
Text(text = "All Task Completed",
fontWeight = FontWeight.Bold,
fontSize = 15.sp,
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp)
)
Text(text = "Nice Work :)", fontSize = 16.sp)
}
}

@Preview(showBackground = true, showSystemUi = true)
@Composable
fun GreetingPreview() {
AllTaskCompleted()
}

3. Compose Quadrant App

// First Approach
package com.appmakerszone.composearticleexercise

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
QuadrantCompose()
}
}
}

@Composable
fun QuadrantCompose() {
Column {

Row(Modifier.weight(1f)) {

Box(Modifier.weight(0.5f)) {
SingleQuadrant("Text composable", "Displays text and follows the recommended Material Design guidelines.", Color(0xFFEADDFF))
}
Box(Modifier.weight(0.5f)) {
SingleQuadrant("Image composable", "Creates a composable that lays out and draws a given Painter class object.", Color(0xFFD0BCFF))
}

}

Row(Modifier.weight(1f)) {

Box(Modifier.weight(0.5f)) {
SingleQuadrant("Row composable", "A layout composable that places its children in a horizontal sequence.", Color(0xFFB69DF8))
}
Box(Modifier.weight(0.5f)) {
SingleQuadrant("Column composable", "A layout composable that places its children in a vertical sequence.", Color(0xFFF6EDFF))
}
}

}
}

@Composable
fun SingleQuadrant(title: String, description: String, backgroundColor: Color) {
Column(
Modifier
.fillMaxSize()
.background(backgroundColor),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = title, fontWeight = FontWeight.Bold)
Text(text = description, textAlign = TextAlign.Justify, modifier = Modifier.padding(15.dp))
}
}


@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
// SingleQuadrant("Title", "This is a description", Color.Blue)
QuadrantCompose()

}

Second Approach:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.composequadrant.ui.theme.ComposeQuadrantTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeQuadrantTheme {
// A surface container using the ‘background’ color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
ComposeQuadrantApp()
}
}
}
}
}

@Composable
fun ComposeQuadrantApp() {
Column(Modifier.fillMaxWidth()) {
Row(Modifier.weight(1f)) {

ComposableInfoCard(
title = stringResource(R.string.first_title),
description = stringResource(R.string.first_description),
backgroundColor = Color(0xFFEADDFF),
modifier = Modifier.weight(1f)
)

ComposableInfoCard(
title = stringResource(R.string.second_title),
description = stringResource(R.string.second_description),
backgroundColor = Color(0xFFD0BCFF),
modifier = Modifier.weight(1f)
)
}

Row(Modifier.weight(1f)) {
ComposableInfoCard(
title = stringResource(R.string.third_title),
description = stringResource(R.string.third_description),
backgroundColor = Color(0xFFB69DF8),
modifier = Modifier.weight(1f)
)

ComposableInfoCard(
title = stringResource(R.string.fourth_title),
description = stringResource(R.string.fourth_description),
backgroundColor = Color(0xFFF6EDFF),
modifier = Modifier.weight(1f)
)

}
}
}

@Composable
private fun ComposableInfoCard(
title: String,
description: String,
backgroundColor: Color,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.background(backgroundColor)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = title,
modifier = Modifier.padding(bottom = 16.dp),
fontWeight = FontWeight.Bold
)
Text(
text = description,
textAlign = TextAlign.Justify
)
}
}

@Preview(showBackground = true)
@Composable
fun ComposeQuadrantAppPreview() {
ComposeQuadrantTheme {
ComposeQuadrantApp()
}
}

4. Business Card App

Code:

package com.appmakerszone.composearticleexercise

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BusinessCard()
}
}
}


@Composable
fun BusinessCard() {
Column(
Modifier
.fillMaxSize()
.background(Color(0xFFD2E8D4)),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,

) {

Box(
modifier = Modifier
.weight(4f)
.fillMaxWidth()
) {
BusinessNameInfo()
}

// This Box will take up the remaining space
Box(
modifier = Modifier
.weight(2f)
) {
BusinessContactInfo()
}

}
}

@Composable
fun BusinessNameInfo() {
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(
id = R.drawable.android_logo
),
contentDescription = "",
modifier = Modifier
.height(110.dp)
.width(110.dp)
.background(Color(0xFF0E3042))
)
Spacer(modifier = Modifier.padding(5.dp))
Text(
text = "Animesh Roy",
fontSize = 40.sp,
fontWeight = FontWeight.Light
)
Spacer(modifier = Modifier.padding(5.dp))
Text(
text = "Passionate Android Developer",
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.SemiBold,
fontSize = 16.sp,
color = Color(0xFF70DD86)
)
}

}

@Composable
fun BusinessContactInfo() {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Bottom,
modifier = Modifier
.fillMaxSize()
.padding(bottom = 50.dp)
) {

Column {
Icon(imageVector = Icons.Rounded.Call, contentDescription = null, tint = Color.Green)
Spacer(modifier = Modifier.padding(6.dp))
Icon(imageVector = Icons.Rounded.Share, contentDescription = null, tint = Color.Green)
Spacer(modifier = Modifier.padding(5.dp))
Icon(imageVector = Icons.Rounded.Email, contentDescription = null, tint = Color.Green)
}

Column {

Text(
text = "+11 (123) 666 444 999",
fontSize = 15.sp,
modifier = Modifier.padding(start = 25.dp)
)
Spacer(modifier = Modifier.padding(10.dp))
Text(
text = "@roy_animesh7",
fontSize = 15.sp,
modifier = Modifier.padding(start = 25.dp)
)
Spacer(modifier = Modifier.padding(10.dp))
Text(
text = "animesh.android@gmail.com",
fontSize = 15.sp,
modifier = Modifier.padding(start = 25.dp, bottom = 4.dp)
)

}

}
}


@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
BusinessCard()
}

Thanks for reading this article :)

--

--