Pocket Studio Academy
HomePart 33.2

Your first composable

Free lesson10 min read·3 questions

A composable is an ordinary Kotlin function wearing one extra label. Write one, call it from your Activity, and it appears on the phone. Then give it parameters and it becomes a piece you can reuse anywhere.

The smallest unit of a screen

In Compose there is no "screen object" and no "view object". There is a function.

You already know how to write functions — you spent Part 1 doing it. A is that same thing with one extra label stuck on the front, and one different job: instead of returning a value, it describes a piece of screen.

Here is a complete, real one:

kotlin
1@Composable
2fun Greeting() {
3    Text(text = "Hello from Compose")
4}

That is not a simplified teaching example. That is a genuine composable, and you could ship it.

Think of it like this

Think of a rubber stamp.

A stamp is carved once. After that you can press it onto a hundred pages and get the same shape every time. If you carve a stamp with a blank box on it, you can write something different in the box on each page — same stamp, different result.

A composable is a stamp for a bit of screen. @Composable is the handle that says "this is a stamp, not a paragraph of ordinary instructions". are the blank boxes.

And exactly like a stamp, it has no memory of the pages it has already printed. That single fact explains almost everything about how Compose behaves — including a puzzle in Lesson 3.5.

What @Composable actually is

@Composable is an — a label attached to your code that some tool reads. This one is read by the Compose compiler, a plugin that runs as part of your build. Your project turns it on with a single line in the build file:

app/build.gradle.ktskts
1plugins {
2    alias(libs.plugins.android.application)
3    alias(libs.plugins.kotlin.android)
4    alias(libs.plugins.kotlin.compose)
5}

That third line is the whole setup. When the Compose compiler sees @Composable, it rewrites your function behind the scenes so it can be tracked, skipped and re-run intelligently. You never see the rewritten version, and you never need to.

The label buys the function two powers:

  1. It may call other composables. Text, Column, Button — all composables. Only a composable can call one.
  2. It may read state and be re-run automatically when that state changes. That is Lesson 3.5 and 3.6.

And it comes with one restriction, which follows from those powers: a composable can only be called from another composable. Try it from an ordinary function and the compiler stops you immediately. That error is in the Error Doctor below, and you will meet it for real.

Three rules for writing one

  • Name it like a thing, not an action. PlayerPanel, not drawPlayerPanel. Composables are named in PascalCase with a capital first letter, unlike every other Kotlin function you have written. This is deliberate: a capital tells you at a glance that a call puts something on screen.
  • Return nothing. A composable's type is Unit, which is Kotlin's way of saying "no useful value". It does not hand you a screen. It emits one, as a side effect of running.
  • Take parameters for everything that varies. That is what turns one function into a piece you use in ten places.

Making it appear

A composable does nothing on its own. Something has to call it, and the first call comes from your :

MainActivity.ktkotlin
1package com.yourname.composelab
2
3import android.os.Bundle
4import androidx.activity.ComponentActivity
5import androidx.activity.compose.setContent
6import androidx.compose.material3.Text
7import androidx.compose.runtime.Composable
8
9class MainActivity : ComponentActivity() {
10    override fun onCreate(state: Bundle?) {
11        super.onCreate(state)
12        setContent {
13            Greeting()
14        }
15    }
16}
17
18@Composable
19fun Greeting() {
20    Text(text = "Hello from Compose")
21}

Press Run and there it is: black text in the top-left corner of a white screen, with no padding and no styling, because you have not asked for any. Compose gives you exactly what you described and nothing else.

9:41▲ ▮
Hello from Compose

Greeting() on its own. No padding, because none was asked for.

Parameters turn one stamp into many

A Greeting that always says the same thing is not much use. Add a and the same function serves every case:

kotlin
1@Composable
2fun PlayerLine(name: String, score: Int) {
3    Text(text = "$name: $score")
4}

Now call it twice with different :

kotlin
1@Composable
2fun DuelScreen() {
3    Column(modifier = Modifier.padding(24.dp)) {
4        Text(text = "DICE DUEL")
5        PlayerLine(name = "Player 1", score = 12)
6        PlayerLine(name = "Player 2", score = 9)
7    }
8}
9:41▲ ▮
DICE DUEL
Player 1: 12
Player 2: 9

DuelScreen(). Three lines of text in a 24dp-padded column.

That is the whole model. Your composables call other composables, which call other composables, until eventually everything bottoms out in the handful that Compose knows how to actually draw. The result is a tree, and the tree is your screen.

Seeing it without running the app

Building and installing an takes a few seconds even on a fast phone. When you are nudging a number back and forth, that adds up. So Compose has previews:

kotlin
1@Preview(showBackground = true)
2@Composable
3private fun DuelScreenPreview() {
4    DuelScreen()
5}

Two labels: @Preview says "render this without launching the app", and @Composable because it is still a composable. Mark it private — nothing else should ever call it.

A preview function must take no parameters, which is why you write a tiny wrapper that supplies fixed values rather than previewing PlayerLine directly. Notice how easy that is here: because PlayerLine takes its data as parameters, you can preview it with any values you like, including ones that would be awkward to reach in the real app — a score of 999, an empty name, a player called Bartholomew-Fitzgerald. Designing for the preview quietly makes your code better.

Note

Previews are a tooling feature, and how they appear varies between versions of Pocket Studio. If you do not see a preview pane, nothing is broken — press Run instead. Every code sample in this course works either way.

Try it in Pocket Studio

Make a fresh project to play in for the whole of Part 3.

  1. Open Pocket Studio and tap Projects, then New Project.
  2. Choose the Empty Compose Activity template.
  3. Name it ComposeLab. Leave the package as the suggested one, or set it to com.yourname.composelab.
  4. Tap Create. Wait for the first — it is the slow one, as Lesson 2.6 explained.
  5. Open app/src/main/java/…/MainActivity.kt from the file tree.
  6. Delete the generated Greeting function and whatever the template put inside setContent.
  7. Type the DuelScreen and PlayerLine functions from this lesson, and put DuelScreen() inside setContent { }.
  8. Tap the import prompt when Pocket Studio offers it, or add the imports by hand: androidx.compose.material3.Text, androidx.compose.runtime.Composable, androidx.compose.foundation.layout.Column, androidx.compose.foundation.layout.padding, androidx.compose.ui.Modifier, androidx.compose.ui.unit.dp.
  9. Tap Run. Three lines of text should appear.
  10. Change score = 12 to score = 30, Run again, and watch it follow.
Error Doctor5 common errors
e: @Composable invocations can only happen from the context of a @Composable function
MeansYou called a composable — Text, Column, your own one — from a function that is not itself marked @Composable. This is the single most common Compose error there is.
FixAdd @Composable on the line above the calling function. If the call is inside a lambda such as onClick = { ... }, that lambda is not composable, and you need a different approach — see Lesson 3.8.
e: Unresolved reference: Composable
MeansThe annotation itself has not been imported. It lives in the Compose runtime, not in Material 3.
FixAdd import androidx.compose.runtime.Composable at the top of the file.
e: Unresolved reference: Preview
Means@Preview comes from the tooling library, which is a separate import from everything else.
FixAdd import androidx.compose.ui.tooling.preview.Preview, and check app/build.gradle.kts lists implementation(libs.androidx.ui.tooling.preview).
e: Type mismatch: inferred type is Unit but String was expected
MeansYou gave your composable a return type — something like fun Greeting(): String — and then called Text inside it. Text produces nothing to return, so the function's promise is broken.
FixDelete the return type. Composables always return Unit: @Composable fun Greeting() { ... }. If you truly need a value, compute it in an ordinary function and pass the result in as a parameter.
e: None of the following functions can be called with the arguments supplied: public fun Text(text: String, ...)
MeansYou passed something that is not text to Text — usually a number, as in Text(score).
FixWrap it in a string: Text(text = "$score"). Compose will not quietly convert an Int to a String for you, which is a good thing — it means "Score: " + 3 never turns into something you did not intend.
Recap
  • A is a normal Kotlin marked @Composable. It describes a piece of screen instead of returning a value.
  • Name them in PascalCase, like nouns: PlayerLine, DuelScreen.
  • Only a composable can call a composable. Everything starts from setContent { } in your .
  • are what make one composable reusable in many places — and previewable with any values you like.
  • @Preview renders a composable without launching the app. The preview function takes no parameters, so wrap the real one.
  • Next: — how you add padding, size, background and taps to anything on screen, and why the order of the chain changes the result.