June 22

0 comments

Jetpack Compose Start Right

By Val Okafor

June 22, 2023

Jetpack Compose

I started to write an introductory book on Jetpack Compose but couldn’t keep up with the rapid changes in the Compose tooling, so I am condensing and publishing the few chapters I have written for free. Here is what would have been chapter 2 of the book.

Today we will learn the basics of Jetpack Compose fundamentals starting with the Composable function, which is at the heart of Jetpack Compose, and the built-in composables such as Text, layouts, and previews. We can only cover the fundamentals of these components. However, throughout the book, we will expand upon these components. We will also look at the basic structure of a Compose app.

Introduction to Composable Function

Composable functions are the building block of Jetpack Compose. The UI is created in functions that do not return a value. Instead, they write to the screen whatever is described in that function. These functions are annotated with @Composable annotation and must be called from another Composable. Inside your composable function, you can use the numerous built-in composables such as Text, Column, Row, etc., or call your own custom composable.

Composable functions are called often called composables. You can use standard Kotlin syntax and control flow inside a composable. A new Android Studio project will normally create your first composable function called Greetings that accepts a parameter of name as shown in Code snippet 1.1. That is a composable function, and it calls a built-in Text composable to emit the passed-in text to the screen.

@Composable
fun Greeting(name: String) {
   Text(text = "Hello $name!")
}
Code Snippet 2.1 – Hello World Composable

A composable function is special because it is designed to be combined and composed with other composable functions to create more complex UI elements. This approach allows developers to easily reuse UI logic and create scalable UI designs that adapt to changing requirements.

For example, imagine you have a composable function that defines a button with a specific style and functionality. You can then reuse this function in different parts of your app or combine it with other composable functions to create more complex UI elements, like a toolbar or a navigation menu.

Compose Calling Context

All composable functions must be called from another composable function. This is similar to the way all suspend functions must be called from another suspend function. The onCreate of a Composable Activity has a setContent composable block, as shown in Figure 1.2, which can serve as an entry point to your first composable function. From there, you can call other composable functions.

Figure 2.2 – Set Content

A Composer context is implicitly attached as a parameter to all composable functions. The context is passed around, so you are required to call a composable only from another composable so that the calling context is propagated.

Composable Function Type

A composable function is a normal function with an @Composable keyword. Adding this annotation alters the type of the function. It signals to the Kotlin compiler that this function is intended to convert data into UI. A composable function is immutable, can accept parameters, and does not return anything; instead, it emits UI.

The two functions in Code snippet 2.2 are two different types because one is annotated with the @Composable keyword.

//Non Composable Function
fun signInScreen() {
    val onSignInButtonClicked: (String, String) -> Unit = {email, password ->
        Log.i("LoginScreen", "Email: $email, Password: $password")
    }
}

//Composable Function
@Composable
fun SignInScreen() {
    val onSignInButtonClicked: (String, String) -> Unit = {email, password ->
        Log.i("LoginScreen", "Email: $email, Password: $password")
    }
Code Snippet 2.2 – Composable function type

Composables can also be used in the function lambda declaration, and as a function type, as shown in Figure 2.3, a slide from a talk Compose runtime.

Figure 2.3 – Composable Function Types

Declarative UI

You will hear the term “declarative” thrown around quite a bit as you learn Jetpack Compose, so let’s take a minute to understand what it means. In the context of Jetpack Compose, “declarative” refers to the way in which UI components are created and defined. Declarative programming is a programming paradigm that emphasizes using declarations or statements that describe what a program should accomplish rather than explicitly describing how to achieve it.

Declarative UI is a programming paradigm that is used in Android UI development with Jetpack Compose. In contrast to the traditional imperative approach, where you write step-by-step instructions on how to create a user interface, declarative UI allows you to define the desired outcome of the UI, and the system handles the rest.

With Jetpack Compose, you write your UI code in a declarative way by describing what you want to see on the screen. You define the UI hierarchy using Compose functions that generate UI elements such as buttons, text fields, and images. You also use modifiers to style and configure these UI elements.

This approach lets you focus on the UI design and logic rather than the implementation details. Jetpack Compose takes care of the performance optimizations and renders the UI efficiently by only updating the parts that have changed, minimizing the number of operations needed to display the UI.

Let’s say we have a screen to select a client from a list where a Card (Figure 2.4) represents each client.

Figure 2.4 – Select client screenshot

With imperative programming, we should present the list as shown in Code snippet 2.3

private fun showClientList(clientList: List<Client>) {
        if (clientList.isNotEmpty()) {
            hideShowRecyclerView(true)
            binding.recyclerviewClientList.layoutManager = LinearLayoutManager(context)
            adapter = ClientListAdapter(requireContext(), clientList, this)
            binding.recyclerviewClientList.adapter = adapter
        } else {
            binding.recyclerviewClientList.adapter = null
            hideShowRecyclerView(false)
        }
    }

    private fun hideShowRecyclerView(show: Boolean){
        if(show){
            binding.noClientGroup.visibility = View.GONE
            binding.recyclerviewClientList.visibility = View.VISIBLE
        } else {
            binding.noClientGroup.visibility = View.VISIBLE
            binding.recyclerviewClientList.visibility = View.GONE
        }
    }
Code Snippet 2.3: Update Client List – RecyclerView

Notice how we are tracking what to show and when to show it. With a declarative/composed approach, we let Compose do some tracking for us.

if (clientList.isEmpty()) {
        EmptyItemsListScreen()
    } else {
        LazyColumn(){
            items(items = clientList) { client ->
                SelectClientCard(client)
            }
        }
    }
Code Snippet 2.4: Update Client List Compose

While there is a bit of simplification on that code snippet, the point is that with declarative programming, we are more focused on the data that needs to be displayed versus when it should be displayed.

Compose Compiler & Runtime

You cannot listen to any Compose conversation for a few minutes without hearing a reference to the Compose compiler or runtime. Some even use it interchangeably. Let us unpack these two terms a bit. Compose compiler is a Kotlin compiler plugin that transforms @Composable functions that you write using the Compose API and transforms them into a form that can be executed on an Android device.

The Compose compiler generates a tree of composables, which are lightweight functions that describe how to render UI elements. Each composable function takes in data and returns a UI element that describes how that data should be displayed. Combining these functions allows you to create complex UIs in a readable and maintainable way.

The Jetpack Compose runtime executes these composable functions and updates the UI whenever necessary. Compose runtime manages the UI state, updates the UI, and provides a number of built-in features and APIs for handling input events. The runtime is also responsible for integrating with other parts of the Android platform. For example, the runtime includes support for handling touch and gesture events, handling keyboard input, and animating UI elements using the Animatable and AnimatedVisibility APIs.

Together the compose compiler, compose runtime and compose UI form the pillar of the Jetpack Compose system on Android. Figure 2.5 shows an illustration from the book Compose Internals.

Figure 2.5 – Compose architecture illustration.

Built-In Composables

Jetpack Compose has many foundational composables to help you jumpstart your UI development. These include Text, Button, Divider, Spacer, Input, Layout (Box, Column, Row), etc. You are encouraged to break your UI into a library of reusable elements. You can combine your custom composables with any built-in composables to create more complex UI elements. This approach allows developers to easily reuse UI logic and create scalable UI designs that adapt to changing requirements.

For example, imagine you have a composable function that defines a button with a specific style and functionality. You can reuse this function in different parts of your app or combine it with other composable functions to create more complex UI elements, like a toolbar or a navigation menu.

Code snippet 2.5 shows a composable using built-in composables of Row, Icon, Spacer, and Text.

@Composable
fun ClientInfoItem(
    name: String,
    icon: ImageVector,
    description: String) {

    Row(modifier = modifier) {
        Icon(
            imageVector = icon,
            contentDescription = description,
            tint = MaterialTheme.colorScheme.primary
        )
        Spacer(modifier = Modifier.width(8.dp))
        Text(
            text = name,
            style = MaterialTheme.typography.bodySmall,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}
Code Snippet 2.6 ClientInfo Composable

This composable is then reused in another composable, where it is combined with more built-in composables to create, ClientInfoCard as shown in Code snippet 2.7.

@Composable
fun ClientInfoCard(client: Client) {
    Card{
        Column(
            horizontalAlignment = Alignment.Start,
            modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
        ) {
            Text(text = client.name)
            Spacer(modifier = Modifier.height(8.dp))

            ClientInfoItem(
                name = client.email,
                icon = Icons.Outlined.Email,
                description = "Email Icon"
            )

            ClientInfoItem(
                name = client.phoneNumber,
                icon = Icons.Outlined.Phone,
                description = "Phone Icon"
            )

            ClientInfoItem(
                name = client.combinedAddress,
                icon = Icons.Outlined.LocationOn,
                description = "Location Icon")
        }
    }
}
Code Snippet 2.7: ClientInfoCard

Preview

The Preview feature lets you see how your Composable functions will look and behave before running the app on an emulator or device. You enable Preview on a composable by annotating that function with @Preview annotation. It is standard practice to create another composable function, annotate it with @Preview and then call the function that you want to preview inside this new function. This allows you to pass parameters such as the width and height of the preview and the device configuration (such as the theme, orientation, or locale) to use for the preview. You can also pass a mock data to the composable that needs to be previewed, as shown in Code snippet 2.8.

@Preview(widthDp = 320, heightDp = 640)
@Composable
fun ClientInfoCardPreview() {
    ClientInfoCard(client = MockClients.getSampleClients()[0])
}
Code Snippet 2.8 – Preview

In this example, the @Preview annotation is used to create a preview called ClientInfoCardPreview, and the width and height of the preview are set to 320dp and, respectively.

Once the @Preview annotation is added, the preview can be viewed in Android Studio’s split-pane; you will need to rebuild your project for the preview to show. You also need to add Compose tooling dependency for Preview to work. The preview feature requires Jetpack Compose tooling dependency to work. This can help speed up the development process by allowing developers to quickly change their UIs and see the results immediately without running the app on an emulator or device.

Jetpack Compose Development Environment and Basic Structure

Developing with Jetpack Compose is made easy when you use Android Studio with Kotlin. If you’re unsure where to start, head to the Compose quick start page for an up-to-date guide. A key takeaway is that it’s best to use the BOM to manage Jetpack Compose library versions instead of declaring them individually. This way, you can keep all library versions in sync. Also, make sure that you use the correct Jetpack Compose compiler version for your Kotlin version. You can check the Compose to Kotlin Compatibility map to find the right version for your project.

Jetpack Compose consists of 7 maven group ids within androidx, each targeting a subset of functionality. Each group has its own release cycle. Additionally, there are the accompanist libraries, which provide extra Android UI features that haven’t been ported to Jetpack Compose yet. Don’t worry about adding all of these dependencies at the start of your project. Begin with the core libraries and add others as needed.

A basic Jetpack Compose project could start with the dependencies shown in Code Snippet 2.9

dependencies {
    implementation 'androidx.core:core-ktx:1.10.0'
    implementation 'androidx.core:core-ktx:1.10.0'

    def composeBom = platform('androidx.compose:compose-bom:2022.12.00')
    implementation composeBom
    androidTestImplementation composeBom

    debugImplementation 'androidx.compose.ui:ui-tooling'
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.ui:ui-tooling-preview"
    implementation "androidx.compose.material:material"
    implementation "androidx.compose.material:material-icons-extended"
    implementation 'androidx.activity:activity-compose:1.7.1'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4"
    debugImplementation "androidx.compose.ui:ui-tooling"
    debugImplementation "androidx.compose.ui:ui-test-manifest"
}
Code Snippet 2.9: Dependencies

Summary

In Jetpack Compose, a composable function is the building block of the user interface that defines a reusable piece of UI logic. You create a composable function by annotating a regular Kotlin function with the @Composable, which signals to the Compose compiler that this function emits UI. A composable function can and should accept parameters. Composable functions are immutable, so you should not hold a reference to them.

In addition to function declaration, the Composable annotation can also be used in Lambda declarations and as a function type. The Compose compiler, Compose runtime and Compose UI form the core of Jetpack Compose UI toolkit on Android. Composables use a declarative syntax that describes the desired UI layout and behavior rather than relying on imperative code that specifies how to achieve the desired result. This makes it easier for developers to reason about their code and make changes without introducing unexpected behavior.

Val Okafor

About the author

I am a family-first Software Engineer and Creator. I create mobile-first apps that grow businesses and increase efficiency. I learn and share my journey figuring out the money side of code. It is written "Man shall not live by code alone"

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Never miss a good story!

 Subscribe to our newsletter to keep up with the latest trends!

>