Mobile Development 20 min read

Getting Started with Jetpack Compose: Setup, Core Concepts, and Practical UI Development

This article introduces Jetpack Compose, explains why it replaces XML‑based Android UI, walks through environment setup, Gradle configuration, and dependency inclusion, demonstrates the setContent and XML integration methods, explores composable functions, state management, list rendering with LazyColumn, and shares practical tips and reflections on declarative UI development in Android.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Getting Started with Jetpack Compose: Setup, Core Concepts, and Practical UI Development

Introduction

Jetpack Compose is Google’s most significant UI change for Android in recent years, and the future focus of Android UI work. With the beta version now stable, it is the perfect time to learn Compose.

What is Jetpack Compose?

Jetpack Compose is a UI toolkit for building native Android apps using Kotlin and declarative programming.

Why replace XML?

Traditional Android UI uses XML layouts parsed into Views, with Java/Kotlin code calling findViewById to manipulate them. This approach suffers from several drawbacks:

Verbosity – each view requires boilerplate code.

Safety – type casting and missing IDs can cause type‑safety and null‑safety issues.

Coupling – UI updates must be manually triggered for each data change.

Lack of cohesion – XML and Java/Kotlin are separate, making refactoring cumbersome.

Inflexibility – XML syntax makes loops, conditionals, and dynamic UI hard.

Various libraries (ButterKnife, DataBinding, ViewBinding) address parts of these problems but introduce new complexities. Compose solves both by discarding XML, using Kotlin for UI description, and adopting a declarative model where UI automatically reflects data changes.

Exploration

Prerequisites

Because Compose is still in beta, you need the Canary version of Android Studio. There are two ways to try Compose:

Create a new project and select Empty Compose Activity .

Integrate Compose into an existing project by configuring Gradle and adding dependencies.

Gradle Configuration

buildscript {
    ext {
        compose_version = '1.0.0-beta02'
    }
}

android {
    defaultConfig {
        ...
        minSdkVersion 21
    }

    buildFeatures {
        // Enable Compose support
        compose true
    }
    ...
    // Set Java 8 compatibility
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
        useIR = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion compose_version
        kotlinCompilerVersion '1.4.31'
    }
}

Adding Dependencies

dependencies {
    implementation 'androidx.compose.ui:ui:$compose_version'
    // Tooling (preview, etc.)
    implementation 'androidx.compose.ui:ui-tooling:$compose_version'
    // Basic components (Box, Image, Scroll)
    implementation 'androidx.compose.foundation:foundation:$compose_version'
    // Material Design
    implementation 'androidx.compose.material:material:$compose_version'
    // Activity integration
    implementation 'androidx.activity:activity-compose:1.3.0-alpha04'
    // AppCompat integration
    implementation 'androidx.appcompat:appcompat:1.3.0-beta01'
    // ViewModel integration
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03'
    // LiveData integration
    implementation 'androidx.compose.runtime:runtime-livedata:$compose_version'
}

Integrating Compose

setContent Approach

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text(text = "Hello Compose!")
        }
    }
}

This is the recommended way. Instead of the traditional setContentView , Compose provides a setContent extension on ComponentActivity that creates a ComposeView and renders the composable lambda.

new (ComposeView) Approach

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    val existingComposeView = window.decorView
        .findViewById
(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        setParentCompositionContext(parent)
        setContent(content)
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

Using this method you can embed a ComposeView in XML layouts or Fragments. Each ComposeView must have a unique ID for proper savedInstanceState handling.

XML Integration

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout ...>
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Hello XML!" />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

In the activity you call findViewById<ComposeView>(R.id.compose_view).setContent { … } . This method is useful for legacy projects but defeats the purpose of abandoning XML.

Understanding Composable Functions

Composable functions are marked with @Composable . They have no return value, must be side‑effect free, and are idempotent. The framework may execute them out of order, in parallel, or skip unchanged parts to optimise performance.

Out‑of‑Order Execution

@Composable
fun HomePage() {
    Header()
    Body()
    Footer()
}

The framework decides the execution order, so each function must be independent.

Parallel Execution

@Composable
fun MemberList(nameList: List
) {
    var num = 0
    Row {
        Column {
            for (name in nameList) {
                Text(text = "姓名: $name")
                num++
            }
        }
        Text(text = "共${num}人")
    }
}

Writing to a mutable variable like num is unsafe. A thread‑safe version uses nameList.size instead.

Local Skipping

@Composable
fun HomePage(title: String, content: String) {
    Header(title)
    Body(content)
    Footer()
}

If only title changes, only Header recomposes.

Optimistic Recomposition

The framework may start recomposition based on an assumed change; if the data changes again before completion, the previous recomposition is cancelled.

Frequent Execution

Composable functions can be called many times per second (e.g., during animations). Heavy work should be moved out of composables and exposed via mutableStateOf or LiveData .

Practical Example – Feed List UI

The following demonstrates a typical feed‑style list built with Compose.

Layout

Each list item is a card with a vertical column of two texts on the left and an image on the right. In Compose you use Card , Row , and Column :

Column {
    Text(data.projName, style = typography.h5)
    Text(data.marketingInfo)
}

Loading Network Images

Use the Accompanist library to bring Glide or Coil into Compose:

dependencies {
    implementation 'com.google.accompanist:accompanist-glide:0.7.1'
}
GlideImage(
    data = data.projPhotoPath,
    contentDescription = null,
    contentScale = ContentScale.Crop
)

Horizontal Arrangement & Card Styling

Row {
    Column { ... }
    GlideImage(...)
}

Card(
    shape = RoundedCornerShape(8.dp),
    elevation = 5.dp
) {
    Row { ... }
}

Modifier Chain

Modifiers adjust size, padding, click handling, etc. Order matters:

GlideImage(
    ...,
    modifier = Modifier
        .width(100.dp)
        .height(100.dp)
        .clip(shape = RoundedCornerShape(8.dp))
)

Card(
    modifier = Modifier
        .clickable { Log.d("Compose", "NewsItem: " + data.pid) }
        .padding(5.dp)
) { ... }

List Rendering

Compose replaces RecyclerView with LazyColumn , eliminating boilerplate adapters.

@Composable
fun NewsItem(data: ItemBean) {
    Card { /* layout as above */ }
}

@Composable
fun HomePage(dataList: List
) {
    LazyColumn {
        items(dataList, key = { it.pid }) { NewsItem(it) }
    }
}

Using a stable key (e.g., pid ) prevents unnecessary recomposition of unchanged items.

State Management

Compose relies on observable state. Use MutableState<T> via mutableStateOf and remember it with remember :

var value by remember { mutableStateOf(default) }

For larger apps, integrate existing Jetpack components such as ViewModel and LiveData :

val viewModel: MainViewModel = viewModel()
val liveNews by viewModel.news.observeAsState(emptyList())
HomePage(liveNews)

Updating the LiveData triggers UI recomposition automatically.

Reflections

Declarative UI frameworks like React, Flutter, SwiftUI, and Compose are reshaping UI development. Compose’s advantage over Flutter is seamless reuse of existing Jetpack libraries and Kotlin familiarity, making it the ideal entry point for Android developers learning declarative UI.

The main challenge is that composable functions may run in parallel, requiring every composable to be thread‑safe. Currently Android Studio lacks specific inspections for this, placing a heavy burden on developers.

mobile developmentAndroidKotlinDeclarative UIJetpack ComposeCompose
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.