Lesson 1
What `@Composable` changes
Build the mental model for a composable function type and its implicit composer context.
Status: The idea that
@Composablechanges a function’s type and supplies a composition context is Durable . The names and generated calls below are Version-specific at the pinned revisions.
Outcome
You should be able to explain why this function cannot be called from an ordinary function:
@Composable
fun Greeting(name: String) {
Text("Hello, $name")
}
The short answer is not “the annotation turns on magic.” It changes the function’s type. A call now participates in a compiler/runtime protocol.
Start with the observable difference
Greeting returns Unit. It does not return a View, a LayoutNode, or a tree object. Yet it can describe content and later run again at the same logical position.
The runtime needs context for that position. The pinned AndroidX Composable.kt describes a useful model: a composable context is passed implicitly, and it can store information from previous executions at the same logical point.
That is also why a composable lambda has a distinct type:
val content: @Composable () -> Unit = { Text("Hello") }
The annotation is part of the type. A value of () -> Unit is not interchangeable with @Composable () -> Unit.
The five-part mental model
flowchart LR
Source[Kotlin source] --> Type[Composable function type]
Type --> Composer[Implicit Composer context]
Composer --> Groups[Groups and runtime memory]
Groups --> Changes[Calculated changes]
Changes --> Nodes[Target tree]The context is not a global singleton. It is a compiler-facing Composer value threaded through generated code; transformed functions also carry change information. Lesson 2 inspects those parameters.
This explains two common observations:
- A wrapper composable can participate in composition without creating a node.
- The same body can execute again while the target tree keeps existing nodes.
The function describes a transformation in a composition. It is not a constructor for a returned widget.
A source walk: annotation to protocol
The current Kotlin compiler source makes this transformation explicit in ComposerParamTransformer.kt. Its lowering adds a synthetic composer parameter to composable declarations and adds synthetic change parameters. It also rewrites calls to composable functions so the caller supplies the same protocol context.
A captured compiler golden output shows the resulting shape. This is a shortened excerpt from the pinned FunctionReferenceTransformTests/reference.txt:
fun Ref(content: Function3<Int, Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
if (%composer.shouldExecute(%changed and 0b0001 != 0, %changed and 0b0001)) {
Ref(<block>{
sourceInformationMarkerStart(%composer, <>, "CC(remember):Test.kt#9igjgp")
val tmp0_group = %composer.cache(false) {
::Fn
}
sourceInformationMarkerEnd(%composer)
tmp0_group
}, %composer, 0b0110)
} else {
%composer.skipToGroupEnd()
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
Ref(content, %composer, updateChangedFlags(%changed or 0b0001))
}
}
<> is the golden renderer’s placeholder for a compiler-generated integer. The example is captured output, not pseudocode. The excerpt intentionally omits source-information and memoization details; those are not needed for this first mental model.
Try the type boundary
Use a compiler-enabled scratch module and test both directions:
fun ordinary(content: () -> Unit) = content()
@Composable
fun caller() {
ordinary { /* ordinary lambda */ }
val c: @Composable () -> Unit = { Text("ok") }
// ordinary(c) should be rejected: the function types differ.
}
Control: remove @Composable from c; the ordinary call should then type-check, but Text can no longer be called inside that lambda.
Expected observation: the annotation changes both where a lambda may be called and which calls the Compose compiler rewrites.
Interpretation limit: this experiment proves a type/lowering boundary. It does not prove a particular runtime scheduling policy or a node insertion.
Misconception check
“
@Composableis only metadata that tools read later.”
No. The annotation is binary-retained metadata, but it also changes the function type and triggers compiler lowering. The runtime-facing parameters are implementation details, while the type distinction is the durable model.
Check yourself
Why can Greeting return Unit and still update a UI tree? Name the hidden participant that gives the call access to composition memory, then name the separate boundary that eventually mutates target nodes.
Evidence notes
| Claim | Direct evidence | Status |
|---|---|---|
@Composable changes a function or lambda type and models an implicit context | Composable.kt | Durable |
| The compiler adds composer and change parameters to composable declarations and calls | ComposerParamTransformer.kt | Version-specific |
A composable body uses the Composer protocol and may skip or restart | Composer.kt and pinned golden output | Version-specific |
Freshness
Refresh this lesson when the Composable KDoc changes, compiler lowering moves the implicit context, or the compiler/runtime protocol changes shape. Re-pin both repositories before treating a generated signature as current.
Finished this lesson?
Your progress stays only in this browser.