Jetpack Compose Mastery Part 1: A Comprehensive Guide to Building Your First Compose Application

1. Preface

As mobile application development continues to evolve, Google has introduced Jetpack Compose, a brand-new modern toolkit for building native Android UIs. Compose aims to simplify and accelerate the UI development process by using a declarative Kotlin DSL, making the construction of dynamic UIs simpler and more intuitive. We will delve into Jetpack Compose UI in two articles, with this one providing a detailed explanation of the basics required to go from zero to one in creating a Compose application, including foundational syntax, components (Composables), state management, layout system, framework, and development.

2. Basic Syntax

Jetpack Compose uses Kotlin's declarative syntax to build UIs. In a declarative UI, developers only need to describe the state that the interface should display, and the Compose framework takes care of rendering and automatically updating the UI when the state changes.

2.1. @Composable Annotation

In Compose, all UI components are defined by functions annotated with @Composable. These functions can receive parameters, manage state, and can nest calls to other @Composable functions.

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

2.2. Composable Functions

Composable functions can call other composable functions just like regular functions, thus building complex UI hierarchies.

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

2.3. Conditional Statements

Within composable functions, conditional statements can be used to control the visibility of UI elements.

@Composable
fun WelcomeMessage(isLoggedIn: Boolean) {
    if (isLoggedIn) {
        Text("Welcome back!")
    } else {
        Text("Please log in.")
    }
}

2.4. Loops

Loops can be used to create repetitive UI elements, such as list items.

@Composable
fun NameList(names: List<String>) {
    Column {
        for (name in names) {
            Text(name)
        }
    }
}

2.5. Higher-Order Functions and Lambda Expressions

Compose makes full use of Kotlin's higher-order functions and lambda expressions, making event handling and component reuse simple.

@Composable
fun ButtonWithClick(onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("Click me")
    }
}

2.6. Modifiers

Modifiers are used to modify the layout parameters, appearance, and behavior of components. They can be chained together.

@Composable
fun StyledText(text: String) {
    Text(
        text = text,
        modifier = Modifier
            .padding(16.dp)
            .background(Color.LightGray)
            .padding(16.dp) // Add padding again to increase space
    )
}

3. Components (Composables)

In Compose, the UI is built through a series of composable functions (Composables). These functions are marked with the @Composable annotation and can contain other Composables, forming a tree-like structure. Each Composable function represents a part of the UI, such as a button, text, or list, and can receive parameters to customize its behavior and appearance.

3.1. Basic Components

Compose provides a range of basic components, such as Text, Button, Image, etc., which are the building blocks for applications.

@Composable
fun BasicComponents() {
    Text(text = "Hello, Compose!")
    Button(onClick = { /* Handle click event */ }) {
        Text("Click Me")
    }
    Image(painter = painterResource(id = R.drawable.ic_launcher), contentDescription = "Icon")
}

3.2. Custom Components

You can create custom components by combining existing Composables, which allows you to reuse UI code and keep your codebase clean.

@Composable
fun UserProfile(name: String, imageUrl: String) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        Image(painter = rememberImagePainter(imageUrl), contentDescription = "User profile picture")
        Text(text = name)
    }
}

3.3. Component Reusability

Composables can be encapsulated and reused, making it simple to build consistent and maintainable UIs.

@Composable
fun ReusableListItem(title: String, subtitle: String) {
    Column {
        Text(text = title, style = MaterialTheme.typography.h6)
        Text(text = subtitle, style = MaterialTheme.typography.subtitle1)
    }
}

4. State Management

Compose manages UI state through observable state objects. When the state changes, Compose will re-invoke the relevant Composable functions to reflect the latest state. This mechanism ensures that the UI is always in sync with the data.

4.1. Observable State

Compose uses mutableStateOf to create observable states. When the value of the state changes, all Composables that use that state will automatically update.

@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

4.2. State Hoisting

State hoisting is a pattern used to move state up to a higher position in the component tree to facilitate state management and sharing.

@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    Counter(count, onCountChanged = { newCount -> count = newCount })
}

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

4.3. State and Side Effects

Compose provides APIs like LaunchedEffect and rememberCoroutineScope to handle side effects, such as launching coroutines.

@Composable
fun Timer() {
    val scope = rememberCoroutineScope()
    var seconds by remember { mutableStateOf(0) }

    LaunchedEffect(key1 = true) {
        scope.launch {
            while (isActive) {
                delay(1000)
                seconds++
            }
        }
    }

    Text("Timer: $seconds")
}

5. Layout System

Compose offers a flexible layout system that allows developers to combine and customize layouts in a declarative way. It includes a range of predefined layout Composables such as Row, Column, and Box, as well as tools for creating custom layouts.

5.1. Layout Components

Compose provides layout components like Row, Column, and Box to control the arrangement of child components.

@Composable
fun LayoutExample() {
    Column {
        Text("First item")
        Row {
            Text("Second item")
            Text("Third item")
        }
    }
}

5.2. Modifiers

Modifiers are used to adjust layout parameters, add decoration, and handle user input. They can be chained together and customized.

@Composable
fun ModifierExample() {
    Text(
        "Hello, Compose!",
        modifier = Modifier
            .padding(16.dp)
            .background(Color.LightGray)
            .padding(16.dp)
    )
}

5.3. Custom Layouts

While Compose provides many predefined layout components, sometimes you may need to create a custom layout. Compose allows you to do this through the Layout function.

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Logic for measuring and placing children
    }
}

5.4. Themes and Styling

Compose allows you to define themes and styles to maintain consistency in your UI and supports dark mode and other configurations.

@Composable
fun ThemedScreen() {
    MaterialTheme {
        // Use colors and styles from the theme
        Text("Hello, Compose!", style = MaterialTheme.typography.h1)
    }
}

5.5. Previews

Using the @Preview annotation, you can preview the UI of composable functions in Android Studio without having to run the app on a device.

@Preview
@Composable
fun PreviewGreeting() {
    Greeting(name = "Android")
}

6. Framework

6.1. Accompanist

Accompanist is a collection of useful libraries that complement Jetpack Compose, providing functionalities not found in the core library, such as pagination, navigation, and animations.

6.2. MVIKotlin

MVIKotlin is a modern MVI framework for Kotlin that integrates seamlessly with Compose, offering a reactive way to handle the state of applications.

6.3. Dagger-Hilt

Dagger-Hilt is a dependency injection library that can be used in conjunction with Compose to simplify dependency management and communication between components.

7. Compose UI Development

In the AI era, development efficiency for Compose UI can also be improved through AI. For example, Codia AI Code supports converting designs into Compose UI Code.

7.1. Environment Setup

Make sure your Android Studio is up to date, as Jetpack Compose requires the latest tooling support. You can download the latest version of Android Studio from the Android Developer official website.

7.2. Creating a New Project

  1. Open Android Studio and click "Start a new Android Studio project."
  2. Select the "Empty Compose Activity" template.
  3. Enter your application name, package name, save location, and other information.
  4. Choose your target Android devices and the minimum API level (API 21 or higher is recommended).
  5. Click "Finish" to create the project.

7.3. Configuring Dependencies

In your project's build.gradle file, make sure you have added the dependencies related to Compose and the Kotlin compiler plugin.

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    // ...
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion '1.x.x' // Use the appropriate Compose version
    }
}

dependencies {
    implementation 'androidx.compose.ui:ui:1.x.x' // Compose UI library
    // Other dependencies...
}

7.4. Building the UI

In the MainActivity.kt file, you will find a default @Composable function, which is the starting point for your application.

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

@Composable
fun MyApp(content: @Composable () -> Unit) {
    // Theme settings for the application
    MaterialTheme {
        // Content of the application
        content()
    }
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MyApp {
        Greeting("Android")
    }
}

7.5. Running the App

Click the "Run" button on the Android Studio toolbar and select a connected device or emulator to run your app. You should be able to see the text "Hello, Android!" displayed on the screen.

7.6. Adding Interactivity

Let's add a button and some state to make the app more interactive.

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

    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Greeting(name = "Compose")
        Button(onClick = { count++ }) {
            Text("Click me!")
        }
        Text("Clicked $count times")
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewCounter() {
    MyApp {
        Counter()
    }
}

Now, when you click the "Click me!" button, the counter's number will increase, and the UI will automatically update.

7.7. Styling and Layout

Use modifiers to add style and layout attributes.

@Composable
fun Counter() {
    // ...
    Button(
        onClick = { count++ },
        modifier = Modifier.padding(16.dp)
    ) {
        Text("Click me!")
    }
    Text(
        "Clicked $count times",
        modifier = Modifier.padding(16.dp)
    )
}

7.8. Testing and Debugging

Use Android Studio's Compose Preview feature to quickly see what your Composable functions look like. If you encounter issues, you can use the Layout Inspector and Logcat to debug.

7.9. Building More Complex UIs

As you become more familiar with Compose, you can start building more complex UIs, such as using LazyColumn to create scrollable lists, or using Navigation to add navigation between screens.

j