@Composable Functions: How They Work in Jetpack Compose

Quick answer: A composable is a Kotlin function annotated with
@Composablethat describes part of your UI for the data it receives. Compose can call that function again when relevant state changes, so the function should be fast, deterministic, and free of direct side effects.
Every Compose screen is built by calling composable functions. Text, Button, Column, and LazyColumn are composables, but so are the components you write for your own app. The @Composable annotation lets the Compose compiler and runtime track those calls as a UI tree and update the affected parts when state changes.
If you have already configured a project with the Compose BOM, this is the concept that turns ordinary Kotlin functions into a UI.
The smallest composable function
Add @Composable above a Kotlin function, then call other composables inside it:
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun WelcomeMessage(name: String) {
Text(text = "Welcome, $name!")
}WelcomeMessage accepts name as input and describes the resulting UI. It does not create a TextView, hold a reference to a view, or manually update a widget. The Compose compiler transforms composable calls into work that can build and update the UI tree.
Composable functions can call other composables, which makes small components easy to assemble into screens:
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun ProfileHeader(name: String, handle: String) {
Column {
Text(text = name)
Text(text = "@$handle")
}
}This nesting is called the Composition. It is a tree of the composable calls that describe the current UI—not a tree of mutable View objects. Android’s Compose basics codelab is a useful first hands-on exercise for this model.
What @Composable changes
The annotation is more than a label. It permits the function to call other composables and gives Compose the information it needs to maintain that part of the UI over time.
In practice, it creates a few useful rules:
- A composable can be called only from another composable or an approved Compose entry point such as
setContent { }. - UI-emitting composables normally return
Unit. Their job is to describe UI, not construct a view object for the caller. - Compose can run a composable again, skip it, or discard an in-progress recomposition. Treat the function body as a description, not as a one-time lifecycle callback.
- Each place where a composable is called is a separate instance in the Composition. Calling
WelcomeMessage()twice creates two independently tracked UI nodes.
That is why this will not compile:
fun createGreeting() {
WelcomeMessage(name = "Ada") // Not in a composable context.
}Instead, invoke it from a composable parent:
@Composable
fun GreetingScreen() {
WelcomeMessage(name = "Ada")
}UI is a function of state
The most helpful mental model is:
UI = f(state)Pass the current state into a composable. When the value changes, Compose schedules recomposition for composables that read it. A recomposition re-runs the UI description with the latest inputs and applies the necessary UI changes.
Here is a small counter:
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun ClickCounter() {
var clicks by remember { mutableStateOf(0) }
Button(onClick = { clicks += 1 }) {
Text(text = "Clicked $clicks times")
}
}The click callback changes clicks; Text reads clicks; Compose can then update the UI using the new value. remember retains the state while this composable instance remains in the Composition. It is not a replacement for durable screen state or process-death restoration.
For a deeper introduction to the state model, read What Is Jetpack Compose?, then compare the choices in Compose State vs ViewModel.
Make components reusable with state and event parameters
The counter above owns its own state. That is useful for a tiny, self-contained example, but app components are often easier to reuse and test when they receive state and send events upward.
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun QuantitySelector(
quantity: Int,
onIncrease: () -> Unit,
) {
Button(onClick = onIncrease) {
Text(text = "Quantity: $quantity")
}
}The parent owns the state:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun CartScreen() {
var quantity by remember { mutableStateOf(1) }
QuantitySelector(
quantity = quantity,
onIncrease = { quantity += 1 },
)
}This is the foundation of state hoisting: data flows down through parameters and events flow up through callbacks. QuantitySelector does not need to know where quantity comes from, so the same component can work with remember, a ViewModel, a test, or a preview.
Why composables should be side-effect free
Compose does not promise that a composable body runs exactly once or in a fixed order. It may recompose frequently, skip a call when its inputs have not changed, or cancel and restart work when newer input arrives. The official Thinking in Compose guide therefore recommends composables that are fast, idempotent, and free of side effects.
Avoid doing this in the body of a composable:
@Composable
fun RiskyProfile(userId: String, analytics: Analytics) {
analytics.trackScreenOpened(userId) // Can run more than once.
Text(text = "Profile")
}Likewise, do not read disk, start a network request, mutate a ViewModel, or write shared preferences just because the composable was invoked. These actions can repeat or happen for UI that never reaches the screen.
Use a callback for user-driven changes, such as onClick. For work tied to the lifecycle of a composable, use the appropriate effect API. For example, LaunchedEffect launches a coroutine when it enters the Composition and cancels it when it leaves:
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@Composable
fun ProfileScreen(userId: String, loadProfile: suspend (String) -> Unit) {
LaunchedEffect(userId) {
loadProfile(userId)
}
Text(text = "Loading profile…")
}Effects are not a place for all business logic. Keep the work UI-related and preserve one-way data flow. The official side-effects guide explains when LaunchedEffect, DisposableEffect, and SideEffect are appropriate.
Recomposition is not a full screen redraw
When observed state changes, Compose tracks the readers of that state and schedules the relevant work. It does not blindly rebuild every part of the screen. Eligible composable functions whose inputs are unchanged can be skipped.
You should not manually force recomposition. Instead:
- Model the current UI data as observable state.
- Read that state where the UI needs it.
- Update the state from an event, ViewModel, repository result, or controlled effect.
The runtime then decides the correct and efficient update path. Keep function bodies small and avoid expensive calculations in composition. If you need to derive a costly value from inputs, consider caching it with remember when that matches its lifecycle; for broader performance diagnosis, see Fix Compose Recomposition Issues.
API design conventions that help
Follow these patterns when you write your own reusable composables:
Use noun-like PascalCase names for UI
Composable UI components represent a thing on screen, so name them like AccountSummary, EmptyOrdersState, or QuantitySelector. This matches the Compose API style guidance and makes call sites read like a UI tree.
Put Modifier near the start
For reusable UI, accept a Modifier with a default value. It gives callers control over layout, padding, test tags, and behavior without making your component expose every possible setting.
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun UserLabel(
name: String,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier.padding(16.dp)) {
Text(text = name)
}
}Use the caller-provided modifier once, at the outermost element that represents the component. This convention keeps a component flexible without leaking its internal layout details.
Keep required data explicit
Prefer parameters such as isLoading, items, and onRetry over reaching into global objects. Explicit inputs make the component predictable in previews and tests, and they show the component contract at its call site.
Use slots when callers need to provide UI
Many Compose APIs accept composable lambdas, such as content: @Composable () -> Unit. This lets a container control layout while its caller controls the child UI:
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
@Composable
fun SettingsSection(
content: @Composable () -> Unit,
) {
Column {
content()
}
}This is the pattern behind Scaffold, LazyColumn, and many Material components. Use it when customization is genuinely useful; a simple parameter is clearer when the UI variation is small.
FAQ
Can a composable return a value?
It can, but UI-emitting composables usually return Unit. A composable that returns a value is best reserved for Compose-aware helpers, such as remember… functions, rather than a visual component.
Can I call a normal Kotlin function from a composable?
Yes. Call pure helpers freely. Be cautious with helpers that do I/O, mutate shared state, or have other side effects because the composable can run more than once.
Do I need remember in every composable?
No. Use it only for values that should survive recomposition for the lifetime of that composable instance. Prefer parameters for state owned by a parent, and use a ViewModel or saved state when state must survive longer.
Is a composable recreated on every state change?
Compose re-executes the portions of the Composition that may need updating. A function with unchanged inputs can be skipped, so treat recomposition as an implementation detail rather than a signal to manage manually.
Next steps
Start by extracting one repeated UI block into a small composable with explicit inputs, an event callback, and an optional Modifier. Then use it in a real layout—for example, items in a LazyColumn. The next roadmap article covers Compose Preview, where these small, parameter-driven components become especially quick to inspect.