๐ŸŽจ Jetpack Compose

Composable Functions Explained

A composable function looks like an ordinary Kotlin function with an annotation, but it follows different rules: it can run many times, in any order, or be skipped entirely. Understanding those rules is the difference between Compose code that works and Compose code that works reliably.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Beginner
On this page
  1. Anatomy of a well-written composable
  2. The rules composables live by
  3. Slot APIs: composables that accept composables
  4. Modifiers: order matters
  5. Previews
  6. Skipping and stability, briefly
  7. Designing a small component library
  8. Common mistakes
  9. Frequently asked questions
  10. Conclusion

The @Composable annotation tells the Compose compiler plugin to transform your function so it can take part in composition: it can remember values, be re-run when state changes, and be skipped when nothing relevant changed. Writing good composables means respecting a few rules that ordinary functions don't have.

Anatomy of a well-written composable

PriceTag.ktKotlin
@Composable
fun PriceTag(
    pricePaise: Long,                       // 1. required data first
    modifier: Modifier = Modifier,          // 2. first optional parameter: modifier
    discounted: Boolean = false,            // 3. other optional parameters with defaults
    onClick: (() -> Unit)? = null,          // 4. events as lambdas
) {
    val text = remember(pricePaise) { formatRupees(pricePaise) }   // cache derived values
    Surface(
        modifier = modifier.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier),
        color = if (discounted) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.surface,
        shape = RoundedCornerShape(8.dp),
    ) {
        Text(text, Modifier.padding(horizontal = 8.dp, vertical = 4.dp), style = MaterialTheme.typography.labelLarge)
    }
}
  • Named in PascalCase because it emits UI, like a noun: PriceTag, not showPriceTag.
  • Returns Unit. It emits UI rather than returning a View.
  • Accepts a modifier as the first optional parameter and applies it to the root element. This lets callers position and size it.
  • Data in, events out: state comes in as parameters, and user actions go out as lambdas.

The rules composables live by

Properties of composable functions Composable functions can execute frequently, can run in any order, can be skipped when inputs are unchanged, should be idempotent, and should be free of side effects in their body. Run oftenevery frame ofan animation Any ordersiblings aren'tsequential steps Skippableunchanged inputsโ†’ not re-run Idempotentsame input โ†’same UI No side effectsin the body; useeffect APIs
Write composables as if they could be called at any time, any number of times, because they can.
Side effect in the body: don'tKotlin
@Composable
fun ArticleScreen(id: String, viewModel: ArticleViewModel) {
    // โŒ Runs on every recomposition: duplicate analytics events and requests
    analytics.log("article_viewed", id)
    viewModel.load(id)

    // โœ… Runs when entering composition, and again only if id changes
    LaunchedEffect(id) {
        analytics.log("article_viewed", id)
        viewModel.load(id)
    }
}

More on effect APIs in Side Effects in Compose.

Slot APIs: composables that accept composables

Instead of a long list of parameters for every variation, accept @Composable lambdas, called slots. Material's Scaffold, TopAppBar and ListItem work this way.

SectionCard.ktKotlin
@Composable
fun SectionCard(
    title: String,
    modifier: Modifier = Modifier,
    action: (@Composable () -> Unit)? = null,     // optional slot
    content: @Composable ColumnScope.() -> Unit,  // trailing content slot with ColumnScope receiver
) {
    Card(modifier.fillMaxWidth()) {
        Column(Modifier.padding(16.dp)) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text(title, style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f))
                action?.invoke()
            }
            Spacer(Modifier.height(8.dp))
            content()
        }
    }
}

// Usage
SectionCard(title = "Recent orders", action = { TextButton(onClick = onSeeAll) { Text("See all") } }) {
    orders.take(3).forEach { OrderRow(it) }
}

Modifiers: order matters

Modifier orderKotlin
Box(Modifier.padding(16.dp).background(Color.Yellow))   // padding outside: yellow area excludes padding
Box(Modifier.background(Color.Yellow).padding(16.dp))   // background first: yellow includes padding
Box(Modifier.clickable { }.padding(16.dp))              // whole padded area is clickable
Box(Modifier.padding(16.dp).clickable { })              // only the inner area is clickable

Each modifier wraps the next, so read the chain from outside to inside.

Previews

Multiple previewsKotlin
@Preview(name = "Light", showBackground = true)
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Preview(name = "Large font", fontScale = 1.5f, showBackground = true)
@Composable
private fun PriceTagPreview() {
    AppTheme { PriceTag(pricePaise = 129_900, discounted = true) }
}

Previews work best with stateless composables that take plain data, which is another reason to hoist state out of reusable components.

Skipping and stability, briefly

Compose can skip a composable during recomposition if all its parameters are stable and equal to last time. Primitives, Strings, lambdas (in most cases) and data classes with stable val properties are stable. Classes with vars, or interfaces such as List that could be mutable, may be treated as unstable, although the Compose compiler's strong skipping mode, enabled by default in recent versions, relaxes this considerably. Deep dive: Compose Performance Optimization.

Designing a small component library

Design-system components

Wrap Material components (AppButton, AppTextField) to enforce your brand's defaults, and expose a modifier and slots.

Screen vs component

Screen-level composables connect to ViewModels. Components below them stay stateless and previewable.

Lists

Row composables take an item and click lambdas, and they never fetch data themselves.

Common mistakes

  • Not exposing a modifier parameter, or applying it to an inner element instead of the root.
  • Reading a ViewModel deep inside small components.
  • Expensive computations in the body without remember.
  • Mutating external variables from composable bodies.

Frequently asked questions

Can a composable return a value?

Composables that emit UI return Unit. Composables that compute values, such as remember or stringResource, return them and are named in camelCase.

Can I call a composable from a normal function?

No. Composables can only be called from other composables, because they need the composition context the compiler plugin adds.

Why does my composable run twice?

Recomposition can run it many times. That's normal. Make sure the body is cheap and free of side effects.

Conclusion

Composable functions are idempotent descriptions of UI that may run often, in any order, or be skipped. Design them with data in and events out, a modifier parameter, slots for flexibility, and no side effects in the body.

Next step: learn where state should live in State Management in Jetpack Compose.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Jetpack Compose clicks once you build real screens with real state. Practise modern Android UI development on projects that go beyond toy examples.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp