Sitemap
Rituraj Sambherao

Rituraj Sambherao

An avid traveller in the AndroidMeda Galaxy. Obsessed with Enterprise Grade mobile solutions.

What is Jetpack Compose, really?

7 min readFeb 10, 2026

--

This is the Jetpack compose migration series.

Jetpack Compose is Android’s modern UI toolkit where you build UI using Kotlin functions instead of XML layouts.

That sentence is true, but it undersells the real shift.

Compose isn’t “XML but in Kotlin.” Compose is a different contract.

Press enter or click to view image in full size

The contract

UI is a function of state.

Instead of “find views, mutate views, keep them in sync,” you describe what the UI should look like for a given state, and Compose updates the screen when state changes.

That’s the big idea. Everything else is details and debugging.

The mental model: from “imperative” to “declarative”

XML + Views (imperative)

The View system encourages this pattern:

  1. Inflate XML
  2. Find views
  3. Set properties
  4. Update properties when something changes
  5. Hope you updated all the right places

In code, it often looks like this:

binding.progressBar.isVisible = state.isLoading
binding.title.text = state.title
binding.errorText.text = state.errorMessage
binding.errorText.isVisible = state.errorMessage != null
binding.retryButton.isVisible = state.showRetry

binding.retryButton.setOnClickListener {
viewModel.retry()
}

It works. But the cost shows up over time:

  • Updates happen in many places
  • Bugs come from missed updates or conflicting updates
  • UI state and business state get tangled
  • Every new UI state means finding all the places that care about it

Compose (declarative)

Compose encourages a different approach:

  1. Describe the UI for the current state
  2. When state changes, the UI re-evaluates and updates itself
@Composable
fun HomeScreen(
state: HomeUiState,
onRetry: () -> Unit
) {
when {
state.isLoading -> LoadingSpinner()
state.error != null -> ErrorView(state.error, onRetry)
else -> ContentView(state.items)
}
}

That’s the “hello world” mental model you want to carry into migration:

You don’t update views. You update state. The UI is a side effect of state.

A real example: Loading states

Let’s look at a pattern you’ve probably written a dozen times.

The XML way

class ProfileFragment : Fragment() {

private fun render(state: ProfileState) {
when {
state.isLoading -> {
binding.progressBar.isVisible = true
binding.profileContent.isVisible = false
binding.errorLayout.isVisible = false
}
state.error != null -> {
binding.progressBar.isVisible = false
binding.profileContent.isVisible = false
binding.errorLayout.isVisible = true
binding.errorText.text = state.error
}
else -> {
binding.progressBar.isVisible = false
binding.profileContent.isVisible = true
binding.errorLayout.isVisible = false
binding.userName.text = state.user.name
binding.userBio.text = state.user.bio
}
}
}
}

Three states. Nine visibility assignments. Every branch must remember to hide the other states’ views.

Add a fourth state (say, “empty”) and you’re updating four places.

The Compose way

@Composable
fun ProfileScreen(state: ProfileState) {
when {
state.isLoading -> LoadingSpinner()
state.error != null -> ErrorMessage(state.error)
else -> ProfileContent(state.user)
}
}

@Composable
private fun ProfileContent(user: User) {
Column {
Text(user.name, style = MaterialTheme.typography.headlineMedium)
Text(user.bio, style = MaterialTheme.typography.bodyMedium)
}
}

Same logic. One-third the code. And critically: you can’t accidentally leave a view visible when it shouldn’t be.

The UI for each state is isolated. No cross-contamination.

This is the first “aha” moment most teams have with Compose.

So what is a Composable?

A @Composable function is a function that can emit UI.

It takes inputs (state, callbacks, dependencies) and produces UI. You can nest composables inside other composables.

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

Simple, but important: Composable functions should be easy to reason about.

Inputs come in. UI comes out. No hidden side effects.

Think of them as render functions, not objects with lifecycle.

Recomposition: the thing everyone fears at first

Recomposition is what happens when Compose re-runs composable functions to update the UI after state changes.

This sounds scary at first:

“Wait, you’re re-running entire functions every time state changes?”

Yes. But Compose is smart about it.

Recomposition is not “redraw the entire screen.” Compose tracks which state each composable reads, and only re-runs the functions that depend on changed state.

A classic example:

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

Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}

When count changes:

  • The Text("Count: $count") recomposes (it reads count)
  • The Button does not recompose (it doesn't read count)

Two things to remember

  1. Recomposition is normal. It’s not a bug or a performance problem by default.
  2. Your job is to make recomposition cheap and predictable. (We’ll cover how in Part 3.)

For now, the mental model is: when state changes, Compose updates only the parts of the UI that care about that state.

State: where Compose becomes very Kotlin-friendly

Compose leans into Kotlin in a way the View system never really could.

UI state can be explicit

Instead of pulling values from Views scattered across your hierarchy, you represent UI state as a Kotlin data model:

data class LoginUiState(
val email: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null
)

Then your UI reads from that state:

@Composable
fun LoginScreen(
state: LoginUiState,
onEmailChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onLogin: () -> Unit
) {
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = state.email,
onValueChange = onEmailChange,
label = { Text("Email") }
)

Spacer(modifier = Modifier.height(8.dp))

OutlinedTextField(
value = state.password,
onValueChange = onPasswordChange,
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation()
)

Spacer(modifier = Modifier.height(16.dp))

Button(
onClick = onLogin,
enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth()
) {
Text(if (state.isLoading) "Logging in..." else "Login")
}

state.error?.let { errorText ->
Spacer(modifier = Modifier.height(8.dp))
Text(
text = errorText,
color = MaterialTheme.colorScheme.error
)
}
}
}

Notice the shape:

  • UI reads state
  • UI sends events via callbacks (onEmailChange, onLogin)
  • No view mutation
  • No “where did this value get set?” detective work

This is why Compose pairs so well with MVVM and unidirectional data flow.

Compose isn’t React (but your brain can borrow a few concepts)

If you’ve touched React, SwiftUI, or Flutter, Compose will feel familiar.

Get Rituraj Sambherao’s stories in your inbox

Join Medium for free to get updates from this writer.

But keep these differences in mind:

  • Compose is Kotlin-first — no separate templating language, full IDE support
  • It integrates with Android lifecycle concepts — Activities, Fragments, ViewModels
  • It’s not virtual DOM — different runtime model, different performance characteristics

A quick mapping for intuition:

Press enter or click to view image in full size

We’ll talk about effects in later parts. For Part 1, just know that Compose has a clean model for side effects, and you should respect it.

Where XML starts fighting you (and Compose helps)

This series targets people migrating from XML, so let’s call out a few pain points Compose directly addresses.

1. State synchronization

In XML apps, UI state can be spread across:

  • ViewModel
  • Fragment/Activity
  • Data binding logic
  • Custom views
  • Adapters
  • Saved instance state

Compose encourages: state in one place, UI as a function of it.

2. Reuse without inheritance gymnastics

In XML, reuse often looks like:

  • <include> tags
  • Style chains that inherit from other styles
  • Custom views with complex attribute sets
  • Composition via nested ViewGroups
  • And a lot of “why does this padding exist?”

Compose makes reuse feel like writing small functions.

Want a branded button? Write a function:

@Composable
fun BrandedButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
),
modifier = modifier
) {
Text(text, style = MaterialTheme.typography.labelLarge)
}
}

Use it everywhere. No XML style hell. Just call the function.

3. List UIs without adapter ceremony

RecyclerView is powerful. It’s also an entire mini-framework:

  • Adapter
  • ViewHolder
  • DiffUtil
  • ItemDecoration
  • LayoutManager
  • Item animations

Compose lists are simpler:

@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users) { user ->
UserListItem(user)
}
}
}

You’ll still care about performance and keys (especially for animations and state preservation), but the baseline experience is much lighter.

What you don’t need to learn today

Compose has depth. But you don’t need to learn everything to start migrating.

Skip for now:

  • Custom layouts
  • Canvas drawing
  • Animation APIs
  • Modifier deep-dives
  • Advanced recomposition optimization
  • Compose compiler internals

Focus on:

  • The mental model (UI = f(state))
  • Basic composables (Text, Button, Column, Row)
  • How state flows from ViewModel → UI
  • How events flow from UI → ViewModel

The rest comes with mileage.

Where a diagram helps (visual learners, this is for you)

Here’s the mental model as a diagram:

┌─────────────────┐
│ ViewModel │
│ │
│ State: Flow │──┐
└─────────────────┘ │
│ emits state
▼
┌──────────────┐
│ Composable │
│ │
│ UI = f(s) │
└──────────────┘
│
│ emits events
▼
┌──────────────┐
│ ViewModel │
│ │
│ (handles) │
└──────────────┘

State flows down. Events flow up.

This pattern (sometimes called “unidirectional data flow”) is not unique to Compose, but Compose makes it feel natural.

TL;DR checklist

• Compose is Kotlin-first declarative UI for Android
• The core shift: ** UI is a function of state **
• Composables are render functions, not views you mutate
• Recomposition is normal and expected (Compose updates only what changed)
• Compose pairs naturally with MVVM, `StateFlow`, and coroutines
• You don't need to learn everything today — just the mental model

What’s next

In Part 2, we’ll talk about:

  • Why Compose was built in the first place
  • What XML struggles with at scale
  • Where Compose is not the right move
  • When it’s smart to wait (even if Compose is the future)

We’ll also cover the tradeoffs nobody talks about in the marketing docs.

Questions? Disagree with something? Want to argue about RecyclerView? Drop a comment — I read them all.

--

--