Exploring Jetpack Compose: A Comprehensive Introduction

Daniely Murua
wearejaya
Published in
12 min readJun 8, 2023

What is Jetpack Compose?

Jetpack Compose is a set of innovative tools for creating native app interfaces on Android. Until recently, layouts in native projects were created using XML. While many projects still follow this approach, Compose is becoming increasingly popular and is being adopted by major companies. However, this transition brings challenges beyond simply laying out a screen… but let’s take it step by step.

In this article, I will explain the essential parts of how Jetpack Compose works. You will learn the fundamentals of this revolutionary technology. We’ll dive into its key features and understand how Jetpack Compose can transform the way we develop native Android apps. Get ready to uncover the secrets behind this powerful tool!

Advantages

Firstly, what are the advantages of using Jetpack Compose? Let’s take a look at some of them:

  1. Simplicity: Jetpack Compose simplifies the development of user interfaces on Android, allowing you to create beautiful UIs more easily and quickly.
  2. Kotlin instead of XML: With Jetpack Compose, you can write your interfaces using Kotlin, a modern and concise language, instead of relying on complex and verbose XML.
  3. High performance: Jetpack Compose uses an efficient rendering engine that delivers excellent performance, providing a smooth and responsive experience for users.
  4. Less code: With Jetpack Compose’s declarative approach, you need to write less code to build your UI. This means less boilerplate and increased productivity in development.
  5. Powerful tools: Jetpack Compose comes with a set of powerful tools to aid in development, such as Compose Preview, which allows you to preview changes in real-time, and Compose Inspector, which helps with debugging and inspecting the interface hierarchy.
  6. Intuitive use of Kotlin APIs: Jetpack Compose leverages the powerful features of Kotlin, allowing you to use resources like extensions, lambdas, and nullable types to create a UI in a more intuitive and expressive manner.
  7. State-driven UI: With Jetpack Compose, you can create state-driven interfaces where changes in data automatically update the UI. This simplifies state management and facilitates the creation of dynamic and interactive interfaces.

Composable Functions

Composable functions are an essential part of Jetpack Compose. They play a fundamental role in defining the user interface. Everything you want to display on your app’s screen should be placed inside these functions.

@Composable
fun ComposableFunctionName() {
...
}

Each Composable function represents a specific component or part of the interface, and they can be combined or nested to create the complete structure of the screen. This makes the functions highly reusable and flexible.

@Composable
@Preview
fun MyScreenPreview() {
MyScreenContent()
}

@Composable
fun MyScreenContent() {
Column {
Text(text = "Hello, Jetpack Compose!")
MyComponent()
}
}

@Composable
fun MyComponent() {
// Your component code here
}

It is important to note that only other Composable functions can call a Composable function. This hierarchy of function calls allows for an organized and modular construction of the interface layout.

Composable functions are declarative, which means you describe how the UI should be rendered based on the state of the data, and Jetpack Compose automatically updates the UI when that state changes.

As shown in the examples above, to designate a function as a Composable function, you simply add the `@Composable` annotation above the function. This annotation informs Jetpack Compose that the function is responsible for defining the user interface.

Behind the scenes, a Kotlin compiler plugin is responsible for transforming these Composable functions into UI elements understandable by the Android system. This process occurs during code compilation and allows the functions to be converted into native Android elements.

Preview

An incredibly useful feature of Jetpack Compose is the ability to preview your component or screen in real-time, even before running the application. To do this, you simply add the `@Preview` annotation above the desired function. The component preview will be displayed in the “Split” section of the development environment, allowing you to instantly visualize the changes made in your code. This feature speeds up the development process as you can iterate and adjust the interface design quickly and efficiently.

@Composable
@Preview
fun MyComponentPreview() {
MyComponent()
}

@Composable
fun MyComponent() {
// Your component code here
}
Component Preview

Design

Jetpack Compose fully supports the principles of Material Design, allowing you to utilize custom themes and define essential elements such as colors, typography, and shapes. This enables you to create a visually consistent experience that aligns with Material Design standards. With Jetpack Compose, you have complete control over the appearance of your interface, allowing you to customize and adjust themes according to your application’s needs.

Additionally, Jetpack Compose offers a wide selection of pre-built components that adhere to Material Design guidelines, simplifying the creation of attractive and intuitive interfaces.

@Composable
fun MyTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> DarkColorScheme
else -> LightColorScheme
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
setContent {
MyTheme() {
MyApp() //Composable function
}
}

Another interesting advantage is the ease of enabling the dark theme in Jetpack Compose. With just a few configurations, you can offer users the option to switch between the light theme and the dark theme in your application, as shown in the code above.

Behavior

As mentioned earlier, Jetpack Compose uses composable functions to transform data into user interfaces. This process is known as Composition. During composition, composable functions are executed to build the UI hierarchy based on the provided data. This is when visual elements are created and reactively updated, ensuring that the interface reflects real-time changes in the data.

“Composition is a description of the UI built by Jetpack Compose when it executes composables.”

However, when creating our layout, it’s common for certain elements to need to be changed dynamically. For example, we may want to change the state of a button from “enabled” to “disabled” while a loading process is taking place on the screen.

When using XML to define our layout, we’re essentially instructing the system on how the views should be rendered in a given state. We set the properties and configurations of the views in XML, and the system takes care of rendering them based on those specifications.

However, with Jetpack Compose, the approach is slightly different.

All UI elements are represented by functions instead of objects. This means there is no direct reference to the elements themselves, and we cannot call them directly to make changes. Instead of manually specifying the view configurations in XML, we use composable functions that are controlled by states and parameters. This follows a reactive pattern, where composable functions are automatically updated based on changes in states and parameters.

So, when we use Jetpack Compose, we can update the state of a button, and its appearance will automatically be updated to reflect that change. We don’t have to explicitly specify how the view should be rendered in each state because Jetpack Compose handles it automatically and reactively.

This is one of the main differences between using XML and Jetpack Compose, making the latter much more intuitive. In XML, manually manipulating views to handle error states, for example, can be a risky approach, leaving room for potential bugs or unexpected behaviors. Imagine if we forget to disable a purchase button in a specific scenario that wasn’t previously accounted for?

Dealing with these state changes can be a significant challenge using XML, but Jetpack Compose solves this problem. Pretty cool, right? Let’s try to visualize this difference in practice.

Let’s build a button that will increment the number displayed on the button text, and when it reaches 10, the button should be disabled, as shown in the gif below.

How would we do this using XML? Thinking in a straightforward way, we would need to have our XML in a separate file and in our activity/fragment, obtain the instance of the button. Once done, we would add a listener to listen for the button click event and within it, increment the count, enable/disable the button when necessary, and change the text, all referring to the instance of the object created from the XML.

val count = 0

fun onClickButton() {
count++
button.enable = count < 10
button.setText("Value: ${count}")
}

What would happen if the “count” variable had the value of 10 and, in an unforeseen scenario, it was reset? The text would still display the value “10” and the button would remain disabled, preventing user interaction. Now, imagine if the layout could react to any change in the “count” state…

Let’s take a look at Compose:

@Composable
fun MyButton() {
val count = mutableStateOf(0)

Button(
onClick = { count.value++ },
enabled = count.value < 10
) {
Text(text = "Value: ${count.value}")
}
}

We have a mutable variable “count” that starts with the default value of 0, and then we construct the button, specifying what should be done when the button is clicked, when it should be enabled/disabled, and the text that the component should display. All these elements depend on a single state called “count”.

Simply put, a state in an Android app is a value that can be changed over time. It can be a value from a database, a variable in a class, or any other data source. This state is responsible for determining what the user interface will display at a given moment and how it will behave.

In the mentioned example, “count” is the state. As this state changes, the corresponding composable function is triggered. However, for this to work correctly, the state needs to be mutable, meaning it can be changed as needed. This is where Compose’s state APIs come into play.

Compose provides observable types called MutableState, which are integrated into the runtime. We can use them to create mutable state variables. When a change occurs in these states, Compose automatically notifies the event handler, resulting in the update of the user interface.

val count = mutableStateOf(0)

But there’s an issue with the code above. If the composable function is triggered again due to a state change, the value of “count” will be reset each time, and our code won’t work correctly.

To avoid this behavior, we can use the Remember API. Simply add the inline composable function called “remember” and wrap the initialization of the variable. This way, it will be retained across calls and won’t be reset.

val count = remember { mutableStateOf(0) }

Here’s a tip: you can also use the “by” keyword in Kotlin, which utilizes property delegates. This will prevent you from needing to use “.value” every time you want to access the current value of the MutableState.

val count = remember { mutableStateOf(0) }
//count.value

val count by remember { mutableStateOf(0) }
//count

While “remember” is capable of retaining state during new calls, it doesn’t preserve the state in case of configuration changes, such as device rotation or theme changes. To ensure that the state is maintained in these situations, we should use the “rememberSaveable” API, which persists the state in a Bundle.

Additionally, “rememberSaveable” is especially useful for retaining the state of items that are not directly involved in a composition. For example, we can use this API to preserve the state of a checkbox in a listing.

val count by rememberSaveable { mutableStateOf(0) }

Recomposition

In previous topics, we have discussed multiple times about the “new calls” of composable functions, which aim to modify components according to state or parameter changes. This process of re-executing functions is known as recomposition.

Jetpack Compose utilizes a UI update loop to maintain synchronization between the state and the display on the screen. When an event occurs, the event handler is triggered, changing the application state. This, in turn, triggers a UI recomposition, where components are updated based on the new state. This cycle of updates between state and UI is repeated continuously, ensuring that the user interface always reflects the current state of the application.

The process of recomposition is optimized to execute only the parts affected by the state change. It performs selective re-execution by identifying which composable functions need to be updated based on the state changes. For this, Compose needs to know which states should be tracked, i.e., which states are relevant for UI updates. This way, Compose ensures efficient performance by avoiding unnecessary re-executions and updating only the relevant parts.

But how does Compose perform this tracking?

Compose utilizes a state tracking system called “Compose’s state tracking system.” It schedules recompositions for any composable that reads a specific state, making Compose granular and capable of responding only to the composable functions associated with that specific state, instead of recomposing the entire screen.

Now that you understand what recomposition is, let’s highlight some important points:

  • It’s important to avoid using external mutable variables that can affect the behavior of a composable function. If a composable function is called with the same state and parameters, it should always produce the same result without any side effects. This ensures consistent and predictable behavior in the application.
  • Composable functions can be executed in any order. Compose has an intelligent mechanism that determines the priority of elements and draws them first. This ensures that the user interface is rendered efficiently and that important elements are displayed correctly, regardless of the order in which the functions are called.
  • Composable functions are executed in parallel, which provides a significant performance advantage, especially on devices with multi-core processors. This means that Compose can make the most of available hardware resources, accelerating the process of rendering the user interface and providing a smoother user experience.
  • Recompositions are handled optimally. When a parameter changes in a composable function, Compose expects the current recomposition to be completed before the parameter changes again. If the parameter changes before the recomposition finishes, Compose cancels the ongoing recomposition and starts a new recomposition with the new parameter. This ensures that the user interface is correctly updated, avoiding issues of inconsistency or undesired behavior.
  • To ensure smooth and uninterrupted animations, it’s important to create fast functions in Jetpack Compose. By optimizing the code and minimizing the execution time of the function, it’s possible to avoid dropped frames and maintain smooth animations. Therefore, when dealing with animations that require frequent updates, make sure to create efficient functions to achieve the best possible performance.

State Drive UI

As mentioned earlier, Jetpack Compose is a declarative framework. Let’s consider a situation where we want to hide or remove a text from the screen when the state of a variable is equal to 0. If we were using XML, we would have to manually manipulate the visibility of the button or remove the view from the layout. However, in Jetpack Compose, instead of performing these manual manipulations, we describe how the interface should be in each state.

This results in the component being automatically added or removed from the composition during recomposition. In the case of our example, when the state has a value of 0, the text element is removed from the component tree. This behavior can be visualized in the Layout Inspector of Android Studio.

In summary, if a component has been called during composition or recomposition, that element is added to the tree. If it hasn’t been called, it is removed. This approach makes Compose more flexible and allows for better organization and control of the user interface.

This behavior applies not only to UI elements but also to variables managed with “remember”. If not called, their memory space is deleted.

Stateful composable x Stateless composable

When a composable function has internal state, we call it a stateful composable. Generally, these composables tend to be less reusable and more difficult to test since they depend on state. On the other hand, composable functions that don’t have any internal state are called stateless composables, which are easier to reuse and test.

A good practice is to avoid creating stateful composables when the state doesn’t need to be maintained internally. Instead, it is recommended to perform state hoisting, which is the process of transforming stateful composables into stateless composables.

“State hoisting” is a pattern of moving state to a composable’s caller to make a composable stateless.”

State hoisting involves moving the necessary state to the parent component and passing it as a parameter to the child composable. This makes composables more flexible and reusable as they no longer depend on specific internal state. Instead, they receive the state as input, allowing them to be used with different states in different contexts.

This practice brings several benefits, such as simplifying composables, better separation of responsibilities, and easier testing. Additionally, it makes the code clearer as state dependencies are explicitly defined.

It is very common that when passing these states to the caller and having a listener inside the internal composable function, we need to update that variable in some way. However, since we passed the variable upwards, we lose direct access to it. In this case, we can use the pattern known as Unidirectional Data Flow (UDF), where we use a lambda function as a parameter. This function will be used to respond to the event, and when triggered, the calling function will make the appropriate change in the value. This way, we maintain the integrity of the unidirectional flow of data.

Now that you understand the essential parts of Jetpack Compose, it’s time to put it into practice. Google offers various codelabs that detail how to build layouts using Jetpack Compose. Take this opportunity to deepen your knowledge and experiment hands-on. I wish you good luck on your learning and development journey with Jetpack Compose.

References

https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-1

--

--

Daniely Murua
wearejaya

Mobile engineer at Jaya Tech and gaming enthusiast. 📱🎮