Jetpack Compose Basics: From Zero to Hero in UI Design

Novjean Kannathara
7 min readJun 20, 2024

--

Jetpack Compose is revolutionizing how we build UI in Android. If you’ve been working with traditional Android development, the move to Compose might feel a bit like switching from a bicycle to a sports car. Strap in, and let’s take a spin through the basics of Jetpack Compose!

What is Jetpack Compose?

Jetpack Compose is a modern toolkit for building native UI. It’s fully declarative, which means you describe your UI in code, and Compose takes care of the rest. This is a departure from the imperative style of the Android SDK, where you modify the UI by calling methods on UI components. With Compose, you simply declare what you want your UI to look like, and it reacts to state changes.

The Basics: From setContentView to setContent

In traditional Android development, you use setContentView to set your layout. In Jetpack Compose, you use setContent.

setContent {
Greeting("Android")
}

Here, Greeting is a composable function. Composable functions are the building blocks of Compose UI.

Creating Reusable Components

One of the core principles of Compose is breaking your UI into reusable components, much like in Flutter. For example, a simple greeting component looks like this:

@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}

Columns and Rows: Organizing Your Layout

Columns and Rows are fundamental building blocks in Jetpack Compose. They allow you to arrange your UI elements vertically and horizontally, respectively.

Using Column

A Column arranges its children in a vertical sequence. Here's a simple example:

@Composable
fun SimpleColumn() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "First item")
Text(text = "Second item")
Text(text = "Third item")
}
}

In this example, we use a Column to arrange three Text composables vertically, with some padding around the edges and spacing between each item.

Using Row

Similarly, a Row arranges its children in a horizontal sequence. Here’s a quick example:

@Composable
fun SimpleRow() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(text = "First item")
Text(text = "Second item")
Text(text = "Third item")
}
}

Just like Column, Row allows for padding and spacing but arranges the elements horizontally instead of vertically.

Modifiers: Customizing Your UI

Modifiers in Jetpack Compose are powerful tools that allow you to customize the look and behavior of your UI components. They can be used to adjust the layout, appearance, and interactivity of your composables. Let’s dive into two commonly used modifiers: background and fillMaxSize.

background: Adding Color and More

The background modifier is used to set the background color (or background image) of a composable. This is particularly useful for setting the visual appearance of UI components.

fillMaxSize: Occupying the Available Space

The fillMaxSize modifier is used to make a composable occupy all available space in its parent layout. This is particularly useful when you want a component to expand to fill the entire screen or its parent container.

Column(
modifier = Modifier
.background(Color.Green)
.fillMaxSize()
) {
Text("Hello, World!", modifier = Modifier.padding(16.dp))
}

By combining these modifiers, you can create highly customized and responsive layouts. The background modifier sets the visual background of the composable, while fillMaxSize ensures that the composable occupies the entire space available to it. This combination is powerful for creating full-screen backgrounds and other UI components that need to adapt to varying screen sizes.

Handling State in Jetpack Compose

In Jetpack Compose, handling state is a crucial aspect of building dynamic and interactive user interfaces. State represents data that can change over time and needs to be displayed or interacted with in the UI. One of the key tools for managing state in Jetpack Compose is remember.

The Power of remember

The remember function in Jetpack Compose is used to store an object in memory, allowing it to persist across recompositions. Recomposition occurs when the state of a composable changes, and the composable is redrawn to reflect the new state. By using remember, you ensure that certain values or objects are not recreated every time a recomposition happens, thereby improving performance and maintaining state consistency.

Here’s a basic example to illustrate how remember works:

@Composable
fun RememberExample() {
var counter by remember { mutableStateOf(0) }
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = "Counter: $counter")
Button(onClick = { counter++ }) {
Text(text = "Increment")
}
}
}

In this example:

  • remember is used to remember the state of the counter variable.
  • mutableStateOf is used to create a mutable state object that Compose will observe.
  • When the Button is clicked, the counter variable is incremented, and the UI is recomposed to reflect the new state.

How remember Works

When a composable function is called, it can be recomposed multiple times in response to state changes. Without remember, every recomposition would recreate all variables, leading to inefficient and inconsistent behavior.

The remember function solves this by keeping the state intact across recompositions. It works like this:

  1. First Composition: When the composable function is called for the first time, remember stores the initial value or object.
  2. Recomposition: On subsequent recompositions, remember retrieves the previously stored value or object instead of creating a new one.

This ensures that your state variables maintain their values across recompositions, which is essential for creating interactive and dynamic UIs.

Text, Buttons, and Snackbars

In Jetpack Compose, text, buttons, and snackbars are essential UI components that allow you to create interactive and dynamic user interfaces. Let’s dive into how these components work and how you can use them effectively, especially focusing on SnackbarHostState and rememberCoroutineScope.

Understanding Snackbars and SnackbarHostState

Snackbars are transient messages that provide brief feedback about an operation at the bottom of the screen. They are an essential part of material design and can be used to show messages, actions, or errors to the user.

To manage snackbars in Jetpack Compose, you use SnackbarHostState. This state object allows you to show and manage the lifecycle of snackbars.

Here’s how SnackbarHostState works:

  • Creating a SnackbarHostState: You create an instance of SnackbarHostState using the remember function to ensure it persists across recompositions.
  • Showing a Snackbar: You use the showSnackbar method on the SnackbarHostState to display a snackbar. This method is a suspend function, meaning it needs to be called from a coroutine scope.

Here’s an example to illustrate the use of SnackbarHostState:

@Composable
fun SnackbarExample() {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Enter text") }
)
Button(onClick = {
scope.launch {
snackbarHostState.showSnackbar("Hello $text")
}
}) {
Text("Show Snackbar")
}
}
}
}

In this example:

  • Creating SnackbarHostState: val snackbarHostState = remember { SnackbarHostState() } creates a SnackbarHostState instance and remembers it across recompositions.
  • Creating a Coroutine Scope: val scope = rememberCoroutineScope() creates a coroutine scope that is remembered across recompositions.
  • Showing a Snackbar: When the button is clicked, scope.launch starts a coroutine, and snackbarHostState.showSnackbar("Hello $text") displays a snackbar with the text entered by the user.

Understanding rememberCoroutineScope

rememberCoroutineScope is a function that provides a coroutine scope tied to the lifecycle of the composable. This means the scope is automatically canceled when the composable is disposed, preventing memory leaks and ensuring efficient resource management.

Here’s how rememberCoroutineScope works:

  • Creating a Coroutine Scope: You create a coroutine scope using rememberCoroutineScope inside a composable function.
  • Launching Coroutines: You can launch coroutines from this scope to perform asynchronous operations like showing a snackbar.

Here’s an example:

@Composable
fun CoroutineScopeExample() {
val scope = rememberCoroutineScope()

Button(onClick = {
scope.launch {
// Perform some long-running task here
delay(2000)
println("Task completed")
}
}) {
Text("Start Task")
}
}

In this example:

  • Creating a Coroutine Scope: val scope = rememberCoroutineScope() creates a coroutine scope that will be automatically canceled when the composable is disposed.
  • Launching a Coroutine: scope.launch starts a coroutine to perform a long-running task, such as a delay or network request.

Combining Snackbars with Coroutine Scope

Combining SnackbarHostState and rememberCoroutineScope allows you to create responsive and interactive UIs that provide feedback to users in real-time. For instance, you can show a snackbar when a task is completed, as demonstrated in the previous example.

By understanding how to manage state, handle asynchronous operations, and provide feedback through snackbars, you can create sophisticated and user-friendly applications in Jetpack Compose. These tools and techniques are essential for building modern Android apps that are both efficient and delightful to use.

Here’s a complete example that demonstrates text input, buttons, and snackbars together:

@Composable
fun CompleteExample() {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
var text by remember { mutableStateOf("") }

Scaffold(
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
) {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Enter text") }
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
scope.launch {
snackbarHostState.showSnackbar("Hello $text")
}
}) {
Text("Show Snackbar")
}
}
}
}

In this complete example, the user can enter text, and upon clicking the button, a snackbar with the entered text is shown. The use of remember, SnackbarHostState, and rememberCoroutineScope ensures efficient state management and responsive UI updates.

The Takeaway

Jetpack Compose simplifies and modernizes Android UI development. By embracing its declarative approach and powerful composables, you can create beautiful, responsive UIs with less code. Dive in, experiment, and start composing your next great app!

Stay tuned for the next article in this series, where we’ll delve into more advanced Jetpack Compose features, ensuring you’re equipped with all the tools you need to master this powerful toolkit. For more detailed examples and advanced topics, check out the official Jetpack Compose documentation. Happy coding! 🚀

About the Author

Novjean Kannathara is a passionate Mobile App developer with a knack for teaching and a love for clean code. Follow him on Twitter and connect on LinkedIn.

--

--