Skip to content
intermediate Phase 4 · Jetpack Compose

Composable Functions

Write composable functions, understand recomposition, and follow Compose naming conventions.

45m
2 problems
Topic Progress 0%

Writing Composable Functions

Anatomy of a Composable

A composable function is any Kotlin function annotated with @Composable. It describes a piece of UI. Composable functions are hierarchical: small composables are composed into larger ones, forming a tree.

@Composable
fun MessageCard(message: Message) {
    Row(
        modifier = Modifier.padding(16.dp),
        horizontalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        Avatar(message.sender)
        Column {
            Text(
                text = message.sender,
                style = MaterialTheme.typography.titleMedium
            )
            Text(
                text = message.body,
                style = MaterialTheme.typography.bodyMedium
            )
        }
    }
}

Composition Is a Tree

Every composable call adds a node to the composition tree. The nesting in your code directly maps to the UI hierarchy:

MessageCard
  Row
    Avatar
    Column
      Text (sender)
      Text (body)

This tree structure is how Compose knows what to update and what to skip.

Naming Conventions

Compose follows consistent naming conventions:

  • Composable functions use PascalCase nouns: ProfileCard, NavigationBar, SearchBar
  • Modifier parameters are always named modifier and placed first:
@Composable
fun MyComponent(
    modifier: Modifier = Modifier,
    label: String
) {
    Text(
        text = label,
        modifier = modifier.padding(8.dp)
    )
}
  • Lambda parameters come at the end (Kotlin trailing lambda convention):
Card(
    modifier = Modifier.fillMaxWidth(),
    onClick = { /* handle click */ }
) {
    Text("Click me")
}

Composables vs Regular Functions

A @Composable function cannot be called from regular Kotlin code, only from other composable functions or from setContent. This is enforced by the compiler.

// This works
@Composable
fun Parent() {
    Child()  // Child is @Composable
}

// This does NOT compile
fun RegularFunction() {
    Child()  // Error: @Composable call expected
}

Common Patterns

Single-responsibility composables do one thing well:

@Composable
fun UserAvatar(url: String, size: Dp = 48.dp) {
    AsyncImage(
        model = url,
        contentDescription = null,
        modifier = Modifier
            .size(size)
            .clip(CircleShape)
    )
}

Slot-based composables let callers fill specific slots:

@Composable
fun CustomScaffold(
    topBar: @Composable () -> Unit,
    content: @Composable () -> Unit
) {
    Column {
        topBar()
        content()
    }
}

This pattern is used extensively by Material components like Scaffold and AlertDialog.

Recomposition in Detail

When Does Recomposition Happen?

Recomposition is triggered when:

  1. A State or MutableState value that a composable reads changes.
  2. A composable's parent recomposes and passes new parameters.
@Composable
fun Counter() {
    var count by remember { mutableIntStateOf(0) }

    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
    // Only the Button lambda recomposes when count changes
    // Counter itself recomposes because it reads `count`
}

Skipping and Optimization

The Compose compiler analyzes your code and generates skip logic. A composable can be skipped if:

  • All parameters are stable (same instance or equal value).
  • The function does not read any State that changed.
// This composable can be skipped if `user` is stable and unchanged
@Composable
fun UserRow(user: User) {
    Text(user.name)
}

If User is a data class with val properties, it is stable. If it has var properties, it is not.

Common Recomposition Pitfalls

Creating objects inside composable functions:

// BAD: Creates a new list on every recomposition
@Composable
fun BadExample() {
    val items = listOf("A", "B", "C")  // Reallocated every time
    LazyColumn {
        items(items) { Text(it) }
    }
}

// GOOD: Use remember to preserve across recompositions
@Composable
fun GoodExample() {
    val items = remember { listOf("A", "B", "C") }
    LazyColumn {
        items(items) { Text(it) }
    }
}

Expensive calculations without caching:

// BAD: Recalculates every recomposition
@Composable
fun FilteredList(items: List<String>, query: String) {
    val filtered = items.filter { it.contains(query) }
    LazyColumn {
        items(filtered) { Text(it) }
    }
}

// GOOD: Cache with derivedStateOf
@Composable
fun FilteredList(items: List<String>, query: String) {
    val filtered by remember(items, query) {
        derivedStateOf { items.filter { it.contains(query) } }
    }
    LazyColumn {
        items(filtered) { Text(it) }
    }
}

Recomposition Scope

Recomposition happens at the composable function level, not at the individual UI node level. If a composable function reads changed state, the entire function body re-executes, but only the composables inside that actually changed will have their UI updated.

Understanding scope is essential for writing performant Compose UI. Keep recomposition scopes as small as possible by extracting state reads into focused composable functions.

CompositionLocals and Scoped Composables

The Problem with Prop Drilling

As your composable tree grows, passing data through many layers becomes tedious:

@Composable
fun App(user: User) {
    Screen(user = user)
}

@Composable
fun Screen(user: User) {
    Sidebar(user = user)
}

@Composable
fun Sidebar(user: User) {
    UserName(name = user.name)  // User was passed through 3 layers
}

This is called prop drilling. It works for a few levels, but becomes unmanageable in large apps.

CompositionLocal to the Rescue

CompositionLocal lets you implicitly pass data down the tree without explicit parameters:

val LocalUser = compositionLocalOf<User> { error("No user provided") }

@Composable
fun App(user: User) {
    CompositionLocalProvider(LocalUser provides user) {
        Screen()
    }
}

@Composable
fun Screen() {
    Sidebar()  // No parameter needed
}

@Composable
fun Sidebar() {
    val user = LocalUser.current  // Reads the provided value
    UserName(name = user.name)
}

Any composable in the subtree can access LocalUser.current without receiving it as a parameter.

When to Use CompositionLocal

Good use cases:

  • Theme data (colors, typography)
  • Authentication state
  • Navigation controller
  • Application-level dependencies

Avoid using CompositionLocal for:

  • Data that changes frequently and only affects one screen
  • Data that belongs in a ViewModel
  • Simple prop passing where parameters are clearer

Built-in CompositionLocals

Material provides several:

  • LocalContext -- current Context
  • LocalDensity -- screen density
  • LocalLayoutDirection -- RTL/LTR
  • LocalColors -- current color scheme
  • LocalTypography -- current typography
@Composable
fun DensityExample() {
    val density = LocalDensity.current
    val pixels = with(density) { 16.dp.toPx() }
    Text("16.dp = $pixels pixels")
}

Custom CompositionLocal

You can create your own for app-wide concerns:

val LocalAnalytics = compositionLocalOf<AnalyticsService> {
    error("Analytics not provided")
}

// Provide it near the root of your app
CompositionLocalProvider(LocalAnalytics provides analyticsService) {
    MyApp()
}

// Use it anywhere deep in the tree
@Composable
fun TrackScreen(screenName: String) {
    LaunchedEffect(screenName) {
        LocalAnalytics.current.logScreenView(screenName)
    }
}

Quiz

1. What is the naming convention for composable functions in Compose?

Question 1 options

2. Why should you extract state reads into focused composable functions?

Question 2 options

3. What is prop drilling and how does CompositionLocal address it?

Question 3 options

4. Which parameter should always come first in a composable function?

Question 4 options

Flashcards

Question

What makes a composable function skippable during recomposition?

Answer

All parameters must be stable (immutable types or data classes with stable properties), and the function must not read changed State values.

Question

What is the correct order for composable parameters?

Answer

1. Required params, 2. modifier (first optional), 3. other optional params, 4. trailing lambda for content.

Question

When should you use CompositionLocal vs explicit parameters?

Answer

Use CompositionLocal for app-wide data (theme, auth, analytics) that many composables need. Use explicit parameters for data specific to a single component.

Question

What is a slot-based composable?

Answer

A composable that accepts @Composable lambda parameters for specific UI slots, such as Scaffold accepting topBar and content lambdas.

Revision Notes

Key Takeaways

  • 1. Composable functions are Kotlin functions with @Composable annotation that describe UI
  • 2. Recomposition only re-executes functions whose inputs have changed, not the entire tree
  • 3. Follow naming conventions: PascalCase nouns, modifier first, trailing lambdas for content
  • 4. CompositionLocal avoids prop drilling for app-wide data like theme and auth state
  • 5. Keep recomposition scopes small by extracting state reads into focused composables

Interview Tips

  • Explain the difference between composition (initial build) and recomposition (updates)
  • Know why stability matters: unstable types prevent Compose from skipping recomposition
  • Be ready to discuss prop drilling and when CompositionLocal is appropriate
  • Discuss slot-based composables and how Material uses this pattern

Cheat Sheet

Composable Functions Cheat Sheet

Anatomy:

  • @Composable annotation required
  • Composables are PascalCase nouns (e.g., ProfileCard)
  • Modifier is always the first optional parameter
  • Trailing lambdas for content slots

Recomposition:

  • Triggered by State changes or parent recomposition
  • Skipped when all parameters are stable and unchanged
  • Scope: entire function re-executes, only changed UI nodes update
  • Extract state reads into small composables for perf

Stability:

  • val data classes with stable fields = stable
  • var properties = unstable
  • Unstable params force recomposition every time

CompositionLocal:

  • Implicitly passes data down the tree
  • Good for theme, auth, navigation
  • Avoid for simple data passing