Skip to main content
Jetpack Compose Quickstarts

Your First Compose UI in 10 Minutes: A Busy Developer’s Quickstart Checklist for Opolis

You have ten minutes between stand-up and a code review. Your ticket says 'build the user profile card in Compose.' You've read the docs, but the real question is: what do you actually type first? This guide is a busy developer's checklist—not a full tutorial, but a field-tested sequence that gets you from zero to a working, previewable Compose UI in under ten minutes. We assume you know Android basics (Gradle, Activities, XML layouts) but haven't shipped Compose to production. Let's skip the fluff and get your hands on the keyboard. Where This Checklist Fits in Real Work Most Compose quickstarts assume you're starting a greenfield project. In reality, you're probably adding a Compose screen to an existing app, or migrating one fragment at a time.

You have ten minutes between stand-up and a code review. Your ticket says 'build the user profile card in Compose.' You've read the docs, but the real question is: what do you actually type first? This guide is a busy developer's checklist—not a full tutorial, but a field-tested sequence that gets you from zero to a working, previewable Compose UI in under ten minutes. We assume you know Android basics (Gradle, Activities, XML layouts) but haven't shipped Compose to production. Let's skip the fluff and get your hands on the keyboard.

Where This Checklist Fits in Real Work

Most Compose quickstarts assume you're starting a greenfield project. In reality, you're probably adding a Compose screen to an existing app, or migrating one fragment at a time. The checklist here is designed for that exact scenario: you have a running project, you want to add a Compose view without breaking anything, and you need to see results fast.

We'll focus on the minimal dependencies, the one-line host setup, and a single composable that renders a profile card with text and an image. This is the pattern you'll repeat for every screen—so getting it right the first time saves you from debugging layout issues later.

What You'll Have After 10 Minutes

By the end of this checklist, you'll have a working composable that:

  • Renders inside an existing Activity or Fragment via AndroidViewBinding or ComposeView
  • Shows a user name, email, and avatar image
  • Responds to a click event (navigates or shows a snackbar)
  • Previews correctly in Android Studio with sample data

You won't have a complete app, but you'll have a repeatable skeleton that you can copy for the next screen. That's the point: a template you can trust, not a one-off demo.

Core Concepts That Trip Up First-Timers

The biggest mental shift from XML to Compose is thinking in functions instead of hierarchies. In XML, you describe a tree of views; in Compose, you write functions that emit UI. That sounds simple, but three concepts consistently confuse newcomers.

Recomposition vs. Re-layout

In XML, when data changes, you find the view and call setText(). In Compose, the entire function can re-run (recompose) when its inputs change. This is powerful, but it means you must not have side effects inside composable functions—no writing to a database or starting a network request directly in the function body. Use LaunchedEffect or callbacks instead. A common rookie mistake: putting a logging statement that triggers on every recompose, which fills Logcat and slows the UI.

State Hoisting

Compose encourages lifting state up to the caller. Instead of a composable managing its own text field value, the parent holds the state and passes it down. This makes composables stateless and testable, but it feels backwards at first. The rule of thumb: if a composable reads or writes mutable state, ask whether that state should live in the ViewModel or a state holder class.

Modifier Order

Modifiers in Compose are applied in the order you write them. Modifier.padding(16.dp).fillMaxWidth() is different from Modifier.fillMaxWidth().padding(16.dp). The first adds padding, then fills the remaining width; the second fills the width first, then adds padding inside that. This subtlety causes layout bugs that are hard to spot. The fix: always think 'outer to inner'—the first modifier affects the outermost layer.

A Pattern That Usually Works: The Profile Card

Let's build the canonical first composable: a user profile card. This pattern works for most simple screens—a container, some text, an image, and an interaction.

Step 1: Add the Dependencies

In your app-level build.gradle.kts, add the Compose BOM and the UI toolkit. For a project targeting API 21+, use:

implementation(platform('androidx.compose:compose-bom:2024.10.00'))
implementation('androidx.compose.ui:ui')
implementation('androidx.compose.material3:material3')
implementation('androidx.compose.ui:ui-tooling-preview')
debugImplementation('androidx.compose.ui:ui-tooling')

Also enable Compose in the buildFeatures block: compose = true. Sync the project.

Step 2: Host a Composable in an Activity

If you're adding to an existing Activity, use setContent with a ComposeView:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).setContent {
            MaterialTheme {
                ProfileCard()
            }
        }
    }
}

If you're starting fresh, you can use setContent directly in ComponentActivity. The key: wrap everything in MaterialTheme to get default colors and typography.

Step 3: Write the Composable

Create a new Kotlin file ProfileCard.kt:

@Composable
fun ProfileCard(name: String, email: String, avatarUrl: String, onClick: () -> Unit) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .clickable { onClick() },
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            AsyncImage(
                model = avatarUrl,
                contentDescription = "User avatar",
                modifier = Modifier
                    .size(48.dp)
                    .clip(CircleShape),
                contentScale = ContentScale.Crop
            )
            Spacer(modifier = Modifier.width(12.dp))
            Column {
                Text(text = name, style = MaterialTheme.typography.titleMedium)
                Text(text = email, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
            }
        }
    }
}

This uses AsyncImage from Coil (add implementation('io.coil-kt:coil-compose:2.6.0')). For a local drawable, replace with Image(painter = painterResource(id = R.drawable.avatar), ...).

Step 4: Add a Preview

Previews let you iterate without deploying to a device. Add this below the composable:

@Preview(showBackground = true, showSystemUi = true)
@Composable
fun ProfileCardPreview() {
    MaterialTheme {
        ProfileCard(
            name = "Alex Rivera",
            email = "[email protected]",
            avatarUrl = "https://i.pravatar.cc/150?u=alex",
            onClick = {}
        )
    }
}

You should see the card in the preview panel. If the image doesn't load, check the URL or use a local drawable.

Anti-Patterns That Make Teams Revert to XML

Even with a working composable, teams sometimes abandon Compose after a few sprints. The reasons are rarely that Compose 'doesn't work'—it's usually these anti-patterns.

Putting Logic in Composable Functions

It's tempting to write var count by remember { mutableStateOf(0) } inside a composable and update it on click. That works for a demo, but in a real app, state management becomes messy. The anti-pattern: mixing business logic (API calls, database writes) directly in the composable. Instead, use a ViewModel and collect state via collectAsState(). If you find yourself writing LaunchedEffect blocks that call repositories, extract that to the ViewModel.

Overusing remember for Expensive Computations

remember caches values across recompositions, but if the computation is cheap, you don't need it. Worse, some developers use remember { mutableStateOf(...) } for every variable, creating unnecessary state that triggers recompositions. The rule: use remember only for objects that are expensive to create (like database instances) or for state that should survive recomposition (like user input). For derived values, use derivedStateOf or compute them inline.

Ignoring Modifier Order

We mentioned this earlier, but it's the #1 layout bug. A typical mistake: Modifier.fillMaxWidth().padding(16.dp) on a Column that should have padding inside the fill. The fix is to reverse the order: Modifier.padding(16.dp).fillMaxWidth(). When in doubt, draw a box around your composable with .border(1.dp, Color.Red) to see the actual bounds.

Maintenance, Drift, and Long-Term Costs

Compose reduces boilerplate compared to XML, but it introduces new maintenance burdens. Teams that adopt Compose without planning for these costs often find their codebase harder to maintain after six months.

Dependency Version Drift

Compose libraries update frequently. The BOM version you use in January may be outdated by March, and mixing versions (e.g., Compose BOM 2024.01 with Material3 1.2.0-alpha) can cause subtle bugs. The long-term cost: you need a policy for updating Compose dependencies regularly—at least every quarter. Otherwise, you accumulate migration debt that hits when you upgrade Kotlin or AGP.

Preview Maintenance

Previews are great, but they rot if you don't update them. A preview that uses hardcoded sample data might compile but show outdated UI after a refactor. The solution: keep preview functions close to the composable and use parameterized previews with realistic data. Some teams write a PreviewData object that provides sample models, so previews stay consistent.

Testing Complexity

Compose UI tests are more reliable than Espresso tests, but they require a different mindset. You write tests against the composable's state, not against view IDs. The cost: your team needs to learn ComposeTestRule and semantics matchers. If you skip testing, you'll rely on manual QA, which doesn't scale. The recommendation: start with one or two UI tests per screen, focusing on state transitions (e.g., loading → success → error).

Modifier Chains Becoming Unreadable

As a composable grows, the modifier chain can become 10+ lines long. That's hard to read and debug. The anti-pattern is to inline everything. The fix: extract common modifier groups into reusable extensions. For example:

fun Modifier.profileCardStyle(): Modifier = this
    .fillMaxWidth()
    .padding(16.dp)
    .clip(RoundedCornerShape(12.dp))
    .background(MaterialTheme.colorScheme.surface)

This keeps the composable body clean and makes changes easier.

When Not to Use This Approach

Not every screen benefits from Compose. Here are the cases where you should stick with XML or consider a hybrid approach.

Complex Custom Views

If your app has a custom view that does heavy onDraw work (e.g., a charting library with thousands of data points), Compose's recomposition model can cause performance issues. In those cases, keep the custom view in XML and wrap it in AndroidView inside a composable. The profile card pattern above is fine for simple UIs, but for graphics-intensive views, measure first.

Large Legacy Codebases with No Compose Experience

If your team has 10+ developers who have never used Compose, and you have a deadline in two weeks, don't rewrite the entire UI. Instead, add one new screen in Compose as a pilot. The checklist here is for that pilot. If the team struggles, you can always fall back to XML for the next screen. The key is to learn incrementally.

Apps Targeting Very Old API Levels

Compose requires API 21+. If your app still supports API 19 or 20, you cannot use Compose in the main app module. You can, however, use it in a dynamic feature module that targets API 21+. But for a universal app, XML is still the safe choice.

When You Need Pixel-Perfect Layouts from Designs

Compose gives you flexibility, but if your designer expects exact pixel alignment (e.g., a design system with strict spacing tokens), you need to enforce those tokens in code. Without careful discipline, Compose layouts can drift from the design. XML constraints are more rigid, which can actually be an advantage for pixel-perfect implementations. Use Compose only if you have a design system with defined spacing and typography tokens that you can map to Compose's Dp values.

Open Questions and FAQ

After helping several teams through their first Compose screen, these questions come up repeatedly. Here are the answers that usually save the most time.

Should I use remember or rememberSaveable?

Use rememberSaveable when you need the state to survive configuration changes (screen rotation, process death). Otherwise, remember is fine. For ViewModel state, use collectAsState() and don't worry about saveability—the ViewModel handles that.

How do I handle navigation in Compose?

Use the Navigation Compose library. Add implementation('androidx.navigation:navigation-compose:2.8.0'). Define a NavHost with routes, and use navController.navigate() in click handlers. Keep the navController in the Activity or a top-level composable, not in individual screens.

Why is my Compose UI not updating when state changes?

Most likely, you're mutating a MutableList or MutableMap without creating a new object. Compose only triggers recomposition when the state object reference changes. Use mutableStateListOf() or create a new list on each update (e.g., list.toMutableList() then reassign).

Can I use Compose with DataBinding?

Yes, but it's not recommended. DataBinding is designed for XML layouts. If you're already using DataBinding, keep it for existing screens and use Compose for new ones. Mixing both in the same layout file is possible via ComposeView, but it adds complexity. Prefer one paradigm per screen.

How do I test a composable that uses AsyncImage?

Use ComposeTestRule and provide a test image loader (Coil has a test module). Or, better, make the image loader injectable so you can replace it with a fake that returns a placeholder drawable. Test the state (text displayed, click behavior) rather than the image loading itself.

Summary and Next Experiments

You now have a working profile card composable, a mental model for state and modifiers, and a list of pitfalls to avoid. The next step is to apply this pattern to a real screen in your app. Here are three specific experiments to run this week:

  1. Add a second composable—a list of items using LazyColumn. This will teach you about scrolling and item keys.
  2. Move state to a ViewModel—refactor the profile card to read from a UserViewModel using collectAsState(). This prepares you for real data.
  3. Write one UI test—use ComposeTestRule to verify that clicking the card triggers the expected action. This locks in the behavior.

After these experiments, you'll have enough confidence to use Compose for the next sprint's new feature. And when you hit a snag, come back to this checklist—it's designed to be your reference for the first few screens. The ten-minute investment pays off every time you avoid a layout bug or a recomposition nightmare.

Share this article:

Comments (0)

No comments yet. Be the first to comment!