Jetpack Compose Tutorial for Android Developers: Build Modern UIs Faster
Recommended Reading

Jetpack Compose Tutorial for Android Developers: Build Modern UIs Faster

AndroidKotlin
Fix bugs faster! Log Collection Made Easy
START NOW!

Overview of Jetpack Compose

Jetpack Compose is Android’s modern UI toolkit to build native user interfaces with less code in a fast, consistent way, increasing code maintainability and establishing a tight integration with other Jetpack libraries.

Jetpack Compose moves away from the traditional XML-based UI design. It enables you to define your user interface using a closer-to-natural declarative programming paradigm, where developers can describe how they want their user interface elements to look and behave in Kotlin code.

This move streamlines the UI development process while providing a more robust and modular framework to produce responsive, interactive, maintainable user interfaces for Single Page Applications (SPA) or complex modern web applications.

What you’ll learn today

We will explore some advanced concepts and techniques in JetPack. We want to arm you with everything required to attain ‘compose superpowers’ in your Android projects. In this series, we will cover:

  • How to get started with Jetpack Compose on your development environment.
  • Underlying constructs like composable functions, state management and Layout Material Concepts in Compose.
  • Complex, interactive user interfaces using advanced UI patterns and best practices.
  • How to do these with existing Android components and architectures.
  • How to Improve performance and deliver a seamless user experience.

Whether you want to put your existing knowledge of Compose into practice, or enhance your existing skills to create more advanced apps, this article will give insights and practical knowledge you need.

Target audience

This blog is designed for senior and experienced Android developers, as well as developers who are switching from XML-based UI, Kotlin developers interested in expanding their skillset, and developers who are simply curious about the latest advances in Android.

Prerequisites

To get started with Jetpack Compose, it’s essential you have the following tools and libraries:

  1. Android Studio: Android Studio Arctic Fox (2020.3.1) or later which supports Jetpack Compose. Android Studio Download
  2. Jetpack Compose Libraries to use:
    1. Compose UI to build the UI.
    2. Compose Material used for Material Design components.
    3. Compose Tooling used for preview and debugging tools.
    4. Dependencies to add to our build.gradle file.
dependencies {
    implementation "androidx.compose.ui:ui:1.0.1"
    implementation "androidx.compose.material:material:1.0.1"
    implementation "androidx.compose.ui:ui-tooling-preview:1.0.1"
    debugImplementation "androidx.compose.ui:ui-tooling:1.0.1"
}

Kotlin: We need to use Kotlin 1.5.21 or later and should configure the project with this in mind.

plugins {
    id 'org.jetbrains.kotlin.android' version '1.5.21' apply false
}

Basic Knowledge

Before starting the work with Jetpack Compose, you need a solid understanding of:

Kotlin Language: Basic knowledge of Kotlin syntax, how to use the DATA classes and functions, and some OOP programming art. If you are new to Kotlin, I suggest going through the official Kotlin’s official documentation.

Android Development Basics: A basic understanding of Android app components (such as Activity or Fragment) along with a minimum working knowledge of UI development. If you’re just starting out learning Android Development basics, the Android Developer Guide is helpful.

Jetpack Compose Basics: You must be familiar with the very basic concepts of Compose, like Composables, State, and Modifiers. The Jetpack Compose Documentation is great.

Port an existing project to use Jetpack Compose

If you have an existing project and want to adopt Jetpack Compose, here are the steps to migrate it to Jetpack Compose:

Update Gradle Files: Open your project’s build.gradle (Project: project_name) file and change the Kotlin version as well as Compose setting.


buildscript {
    ext.kotlin_version = '1.5.21'
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

Enable the Jetpack Compose in Build File: Open build.gradle (Module: app) and add these lines to enable Compose. We also need all the dependencies.

android {
    compileSdkVersion 30
    defaultConfig {
        applicationId "com.example.yourapp"
        minSdkVersion 21
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
    }

    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion '1.0.1'
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation "androidx.compose.ui:ui:1.0.1"
    implementation "androidx.compose.material:material:1.0.1"
    implementation "androidx.compose.ui:ui-tooling-preview:1.0.1"
    debugImplementation "androidx.compose.ui:ui-tooling:1.0.1"
}

Refactor UI Code to Compose: Begin by converting your current XML-based UI code into Jetpack Compose. Define composable functions to replace your current UI components.

XML Code:

//This the XML code for a TextView
<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello, World!" />

Compose Code:

//THis is compose code to have similar TextView Functionality.
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun Greeting() {
    Text(text = "Hello, World!")
}

Set Content in Activity: In our main activity, we simply assign the content to use Composable Function.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

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

Advanced concepts in Jetpack Compose

Composable functions

Jetpack Compose is built using composable functions as the basic unit. UI components and their behavior are declarative. So, to get the most out of Jetpack Compose, we need a clear understanding of how composable functions work and their lifecycle.

A composable function is used in Kotlin. It is simply the regular Kotlin’s normal function, annotated with @composable, which signals that the Compose compiler should be part of the UI hierarchy.

//Example showing basic structure of Composable.
import androidx.compose.runtime.Composable
import androidx.compose.material.Text

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

Key Points:

@Composable: This annotation has to accompany every Compose function that describes a UI component. It allows the function to communicate with Compose Runtime for efficient lifecycle management and updates.

Function Parameters: Composable functions can take parameters, which enables reusable components. For example: Greeting (name: String), is a function, which says it expects one parameter (name) and uses it to say ‘Hello name’.

Return Type: These composable functions do not return anything. Instead they emit UI elements directly in the emission tree.

Composable Function Lifecycle

The Compose runtime manages the lifecycle of a composable function. It includes the following stages:

  • Initial Composition: This is the first build stage, and means that when a composable function gets called for the first time, Compose runtime will construct an initial UI hierarchy. This process means creating all the UI events that were mentioned in composable functions.
  • Recomposition: Composable functions are meant to be re-executed every time their input parameters or state change. This is referred to as recomposition. The Compose runtime will intelligently only update the parts of the UI that have changed, in a bid to minimize performance overhead.
  • Disposal: Compose runtime disposes of the composable when it is not needed, helping to release resources.
//Example code of recomposition in Compose
import androidx.compose.runtime.*

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }

    Column {
        Text(txt = "Count: $count")
        Button(onClick = { count++ }) {
            Text("Incremented value")
        }
    }
}

@Composable Annotation

Jetpack Compose @Composable Annotation has many benefits, so developers should prioritize its implementation.

Significance

Declarative nature: Based on the declarative principle, Jetpack Compose uses the @Composable annotation. It allows functions to describe what the UI should look like, based on the current state.

Efficient recomposition: Compose runtime marks the functions that are relayed out quickly when their state changes with @Composable preparation.

Part of the Compose framework: Annotating your functions enables combinability with state management, theming, and other parts of the compose system.

Best Practices

Small and focused functions: Composable units should be small, and follow the Single Responsibility Principle. This improves the reusability and maintenance of the code.

Avoid Side Effects: Try to avoid the side effects of composable functions. Composables should be pure functions and not interact with external states directly. Instead, we should use state management built into Compose.

@Composable Correctly: Functions that only directly emit UI elements should be marked composables. Annotation is used to mark a helper function that does not emit UI.

@Composable
fun UserProfile(name: String, age: Int) {
    Column {
        Text(text = "Name: $name")
        Text(text = "Age: $age")
    }
}

@Composable
fun UserScreen(user: User) {
    UserProfile(name = user.name, age = user.age)
}

Advanced layouts and Custom Composables

Jetpack Compose is an Android library that helps create advanced layouts and custom composables, allowing us to create sophisticated design systems and exceedingly complex and idiomatic UIs beyond the basic built-in layout.

In this section, you will learn how to create custom layouts using Layout and Modifier, and grasp the advanced use of ConstraintLayout for more ambitious UI design.

Creating Custom Layouts

The Layout composable in JetPack Compose helps you design custom layouts. This gives you full control over how your composable children are measured and positioned.

Steps to create a custom layout

Create the custom layout: The new Layout can be composed to define a highly customizable single column **RecyclerView-**like functionality, by taking an itemCount block as input.

Measure Children: Measure all the children’s composable size.

Position Children: Decide how to locate each child in the layout.

**//Example of how to create a Custom Layout:**
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@Composable
fun CustomColumn(
    modifier: Modifier = Modifier,
    spacing: Dp = 8.dp,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        // Measure each child
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        // Position children vertically with spacing
        var yPosition = 0
        layout(constraints.maxWidth, constraints.maxHeight) {
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = yPosition)
                yPosition += placeable.height + spacing.toPx().toInt()
            }
        }
    }
}

@Composable
fun CustomLayoutExample() {
    CustomColumn(spacing = 16.dp) {
        Text("Item1")
        Text("Item2")
        Text("Item3")
    }
}

ConstraintLayout in Compose

ConstraintLayout in Compose is similar to the ConstraintLayout in the traditional View system. ConstraintLayout allows you to create more complex layouts, with the ability to position components inside them relatively.

Adding ConstraintLayout Dependency We need to add a dependency for ConstraintLayout in the project’s build.gradle file:

dependencies {
    implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0"
}
**//ConstraintLayout Usage:**
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.Button
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension

@Composable
fun ConstraintLayoutExample() {
    ConstraintLayout(
        modifier = Modifier.fillMaxSize()
    ) {
        // Create references for the composables to position
        val (button, text) = createRefs()

        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier.constrainAs(button) {
                top.linkTo(parent.top, margin = 16.dp)
                start.linkTo(parent.start, margin = 16.dp)
            }
        ) {
            BasicText("Button")
        }

        BasicText(
            "This is a sample text",
            modifier = Modifier.constrainAs(text) {
                top.linkTo(button.bottom, margin = 16.dp)
                start.linkTo(button.end, margin = 16.dp)
                width = Dimension.preferredWrapContent
            }
        )
    }
}

Advanced Constraints

ConstraintLayout supports advanced constraints such as start, end, top, bottom, center, and more. An example of a chain and a barrier is shown below:

//Example of Chain and Barrier
@Composable
fun ConstraintLayoutChainExample() {
    ConstraintLayout(
        modifier = Modifier.fillMaxSize()
    ) {
        val (button1, button2, button3) = createRefs()
        val horizontalChain = createHorizontalChain(button1, button2, button3, chainStyle = ChainStyle.Spread)

        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier.constrainAs(button1) {
                start.linkTo(parent.start)
                end.linkTo(button2.start)
            }
        ) {
            BasicText("Button 1")
        }

        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier.constrainAs(button2) {
                start.linkTo(button1.end)
                end.linkTo(button3.start)
            }
        ) {
            BasicText("Button 2")
        }

        Button(
            onClick = { /*TODO*/ },
            modifier = Modifier.constrainAs(button3) {
                start.linkTo(button2.end)
                end.linkTo(parent.end)
            }
        ) {
            BasicText("Button 3")
        }
    }
}

Theming and styling

Theming and styling are essential to designing a visually appealing user interface that maintains consistent styles throughout the app.

With Jetpack Compose, you can build personalized, composable user interfaces that adapt to various states using tools such as Dynamic Theming, above and beyond traditional themes.

Dynamic theming

This means you can change the design of your app in the event that, for example, a user decides to make a change or change their theme at the system level. The implementation of dynamic theming begins with the MaterialTheme composable.

Dynamic theming: changing MaterialTheme

Define theme colors

//Example to define the theme colors
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.ui.graphics.Color

private val LightColorPalette = lightColors(
    primary = Color(0xFF6200EE),
    primaryVariant = Color(0xFF3700B3),
    secondary = Color(0xFF03DAC6)
)

private val DarkColorPalette = darkColors(
    primary = Color(0xFFBB86FC),
    primaryVariant = Color(0xFF3700B3),
    secondary = Color(0xFF03DAC6)
)

Create a theme Composable: This will set the desired theme.

//Example for custom theme 
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = Typography,
        shapes = Shapes,
        content = content
    )
}

Use the theme in Your Composables: Edit your theme containing the composables of the application.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                Greeting("World")
            }
        }
    }
}

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

Animations in Compose

Animations can be used to create seamless transitions and interactions, which help improve the overall user experience. One of the great things about Jetpack Compose is that you get a variety of APIs to create simple or complex animations.

Basic Animations

There are also corresponding animate*AsState functions in Jetpack Compose for basic animations. These functions enable you to animate things like color changes and size manipulations, or simply change the visibility of any element.

Color Animation example:

import androidx.compose.animation.animateColorAsState
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color

@Composable
fun ColorAnimationExample() {
    var isRed by remember { mutableStateOf(true) }
    val backgroundColor by animateColorAsState(
        targetValue = if (isRed) Color.Red else Color.Blue
    )

    Button(onClick = { isRed = !isRed }, backgroundColor = backgroundColor) {
        Text(text = "Change Color with Animation")
    }
}

Size Animation example:

import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun SizeAnimationExample() {
    var large by remember { mutableStateOf(true) }
    val size by animateDpAsState(targetValue = if (large) 100.dp else 50.dp)

    Button(onClick = { large = !large }) {
        Text(text = "Change Size with Animation")
    }

    Box(modifier = Modifier.size(size))
}

Performance optimization

Mastering the Jetpack Compose performance optimization, recomposition and Android Studio profiling tools will help you gain even better performance and consistency with your Compose application.

Recomposition and performance

Jetpack Compose watches these states and when they change, it recomposes to update the UI using the composable functions of Recomposition.

Recomposition Administration can help make recomposition efficient, and avoid unnecessary recompositions that could decrease app performance. Indeed, the ability to understand and optimize recomposition is one of the most important keys to maintaining a fast, slick Compose app.

Understanding Recomposition

Triggers for Recomposition

If one of the composable functions depends on a state, it recomposes if that particular state changes. When the parameters of a composable function change, recompose it.

Preventing Unnecessary Recompositions

Use remember to Persist State

//Example showing the usage of remember function. 
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

The **r**emember function maintains the state across the recomposition of code, thus eliminating unnecessary re-compositions.

Hoist State to Avoid Recomposition

@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    Column {
        Counter(count, { count++ })
        Text("Other UI")
    }
}

@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
    Button(onClick = onIncrement) {
        Text("Count: $count")
    }
}

The solution is to lift the state up one level so that not all child composables are recomposed.

Key-based Recomposition

Use the key to control recomposition based on specific values.

//Example showing the usage of key function. 
@Composable
fun UserProfile(userId: String) {
    key(userId) {
        // This block will only recompose when userId changes
        Text("User ID: $userId")
    }
}

Using Tools

Steps to profile and optimize performance

  1. Run the Profiler: Launch the CPU Profiler when running your app to start capturing performance data.
  2. Analyze recomposition counts: Consider looking for high recomposition counts in specific composables. Frequent recompositions are often a sign that a performance issue might be present.
  3. Identify performance bottlenecks: Use the flame chart of your profiler tool, along with timeline views, to determine what operations are taking the most time in recompositions.
  4. Optimize Identify issues: Refactor your composable functions to reduce the number of recompositions. Use remember, derivedStateOf, and key-based recomposition wherever it makes sense to do so.
  5. Test and validate: After optimization, evaluate your application again to ensure that you have made the proper adjustments.
@Composable
fun OptimizedUserProfile(userId: String) {
    // Using remember function to cache expensive operations
    val user = remember(userId) { fetchUser(userId) }

    Column {
        Text("Users Name: ${user.name}")
        Text("Users Age: ${user.age}")
    }
}

fun fetchUser(userId: String): User {
    // Simulate fetching user data
    return User(name = "John Doe", age = 30)
}

data class User(val name: String, val age: Int)

Recomposition and profiling in Android Studio will help you identify Android Compose performance improvements for your Jetpack compose applications, and help deliver a smoother user experience.

Best practices for maintaining a compose-based codebase

Modularize your codebase

Divide your app into the smallest reusable composable functions. We recommend making each composable handle a single piece of UI.

// Example of breaking down into smaller functions.
@Composable
fun ProductCard(product: Product) {
    // Single responsibility for displaying product card
}

@Composable
fun ProductList(products: List<Product>) {
    LazyColumn {
        items(products) { product ->
            ProductCard(product)
        }
    }
}

State management

Opt for a ViewModel and live data or StateFlow management libraries in your app.

Remember between recompositions

Use remember for a state which enforces recomposing.

RememberSaveable-across-configuration-changes

Be sure to use remember and Saveable.

Separation of concerns

Keep UI and business logic separated. Use composable functions for UI rendering and keep your logic in a ViewModel.

Performance Optimization

Prevent recompositions that are not required (and could be very expensive) by using remember and derivedStateOf.

Stay vigilant over your application

Android Studio profiling tools can help you flag performance bottlenecks.

Theming and Styling

Create a uniform color palette for your app with MaterialTheme and theming. To make composables flexible and reusable, we can parse in some style parameters.

Testing

Unit test your ViewModel and other business logic.

@Test
fun testProductCard() {
    val product = Product("Product", 10.0, "url")
    composeTestRule.setContent {
        ProductCard(product)
    }
    composeTestRule.onNodeWithText("Product").assertIsDisplayed()
    composeTestRule.onNodeWithText("$10.0").assertIsDisplayed()
}

Documentation and code comments

Use Composable and parameter documentation for better sustainability, and keep providing explanations with code comments and complex logic.

Summary

In this guide, we discussed building and optimizing Android apps with Jetpack Compose. Here’s a little summary of the main takeaways:

  • This article should provide the knowledge and techniques necessary for a solid foundation. We covered the essentials of what you need to know before diving in with Jetpack Compose, including a step-by-step guide on the installation of Android Studio, a look at how to create new projects with Compose, and migration of existing projects.
  • Advanced Layouts and Custom Composables, where we shared what it takes to create custom layouts using Layout or Modifiers without performance penalties, as well as using ConstraintLayout for more advanced layout designs. We also covered dynamic theming using MaterialTheme and creating your customized themes and styles, resulting in a visually consistent app UI.
  • We covered simple animations using animate*AsState , as well as some more advanced animations with Transition, Animatable, and Gesture APIs. We also discussed approaches to integrating new compose code in the existing Android views/writing pieces of UI, and embedding composables into traditional Views or Fragments.
  • Finally, we looked at recomposition and how to optimize it, as well as Android Studio tools that can profile/recap the performance of Compose.

What an exciting time to be an Android developer!

Expect the Unexpected! Debug Faster with Bugfender
START FOR FREE

Trusted By

/assets/images/svg/customers/projects/eye-d.svg/assets/images/svg/customers/highprofile/tesco.svg/assets/images/svg/customers/cool/starbucks.svg/assets/images/svg/customers/highprofile/axa.svg/assets/images/svg/customers/cool/airmail.svg/assets/images/svg/customers/projects/menshealth.svg/assets/images/svg/customers/highprofile/oracle.svg/assets/images/svg/customers/highprofile/dolby.svg

Already Trusted by Thousands

Bugfender is the best remote logger for mobile and web apps.

Get Started for Free, No Credit Card Required