Welcome to the most exciting phase of the course! You have set up Android Studio (lessons 01 to 05) and learned the Kotlin you need (lessons 06 to 14). Now you finally start building screens. Today you meet Jetpack Compose, the modern toolkit that Google recommends for building Android user interfaces, and you use it to make a real, good-looking birthday card app.
Picture a sign-writer in the market painting a shop board. Old-style Android was like giving the sign-writer a long instruction sheet written in a separate language (XML) and then telling a second person (Kotlin) to go and change the paint later. Compose is like the sign-writer simply describing the board in one language: "a big bold greeting, and under it, a smaller name, to the right." If the greeting changes, you describe the board again and Compose repaints what needs repainting.
By the end of this lesson you will be able to:
- Explain what Jetpack Compose is and what "declarative UI" means.
- Read the code of a freshly generated Compose project:
MainActivity,setContent, the theme andSurface. - Write your own composable functions and follow the rules for them.
- Show and style text with
Text,fontSize,fontWeight,color,textAlignandlineHeight. - Explain the difference between
spanddp. - Preview your UI without running the app, using
@Preview. - Arrange several composables with
ColumnandBox, and give them space withModifier. - Move text into
strings.xmland show it withstringResource. - Build the Birthday Card app and a second Compose Article app.
How this fits the bigger picture: This lesson covers three items from Unit 1, Pathway 3 of Google's Android Basics with Compose course: Intro to Jetpack Compose, Design a birthday card app and Build a simple app with text composables (the "Compose Article" practice app). We follow the same ideas with our own code and designs. Links to the official pages are at the end.
Costs and requirements
Compose previews and the emulator both use memory. If your laptop has 8 GB of RAM, close your browser tabs before running the emulator, or use a real phone as you learned in lesson 05. Previews (this lesson's star feature) are lighter than the emulator, so you can do a lot of your work without launching it.
1. A working Android Studio project (lessons 02 and 03)
You need Android Studio installed and the ability to create an Empty Activity project and press Run. If you cannot yet, go back to lesson 03, Create Your First App.
2. Functions and parameters (lesson 08)
A Compose screen is built from functions. You must be comfortable with fun, parameters, default values and named arguments such as Text(text = "Hi", fontSize = 20.sp).
3. Lambdas and trailing lambda syntax (lesson 12)
Compose code leans on lambdas. When you see this:
Column {
Text("One")
Text("Two")
}
the braces after Column are a lambda passed as the last argument, written outside the parentheses. Inside it, you list the children. This is exactly the trailing lambda syntax from lesson 12.
4. Strings and templates (lesson 06)
"Hello $name" inserts a value into text. Android has its own, translation-friendly version of this idea using strings.xml, which you meet later in this lesson.
What is Jetpack Compose?
Jetpack Compose is Android's modern UI toolkit. You write your user interface entirely in Kotlin, by calling functions that each describe a piece of the screen. It is part of Jetpack, Google's family of Android libraries that you add to a project as dependencies.
Declarative versus imperative
| Style | You write | Analogy |
|---|---|---|
| Imperative (old Views) | Step-by-step commands: find the label, set its text, change its colour | Directing a painter stroke by stroke |
| Declarative (Compose) | A description of what the screen should look like for the current data | Handing the painter a photo of the finished picture |
In Compose you do not reach into the screen and tweak it. You write a function that says "given this name, show this greeting." When the data changes, Compose calls your function again and updates the screen. Lesson 17 and 19 show how data changes trigger that, using state.
A composable function
A composable function is an ordinary Kotlin function with the @Composable annotation. It does not return the UI. It emits UI into the tree that Compose is building.
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
The rules to remember:
| Rule | Why |
|---|---|
Add @Composable above fun. |
It tells the Compose compiler this function describes UI. |
Composables can only be called from other composables (or from setContent). |
Compose needs to be tracking the call. |
Name them with a noun in PascalCase: Greeting, BirthdayCard. |
It is the Compose style guide's convention, and it makes them look like UI elements. Normal functions use camelCase verbs. |
Accept a modifier: Modifier = Modifier parameter. |
It lets the caller control size and position. You will see this pattern everywhere. |
| Do not rely on a return value. | A composable normally returns nothing (Unit). |
The UI tree
Composables call other composables, so your screen is a tree:
setContent
└── BirthdayCardTheme (colours, fonts)
└── Surface (a background)
└── BirthdayGreeting
├── Text("Happy Birthday Ada!")
└── Text("From Tunde")
Each branch is a function call. The leaves are the things the user actually sees, such as Text.
Why composables are cheap to repeat
Because the UI is only a description, calling a composable many times costs little. You can make a Greeting for ten names with a loop. Compose also skips work: when data has not changed it does not redraw that part. This is called recomposition and lesson 19 explains it properly.
Step 1: Create the project
- In Android Studio, choose File > New > New Project (or New Project on the Welcome screen).
- Pick the Empty Activity template. (Recent versions also offer an Empty Views Activity. That one is the old XML style. Make sure you choose the one that mentions Compose or is simply named Empty Activity. Template names move between Studio versions, so read the template's description.)
- Name:
Birthday Card. Package name:com.techbase.birthdaycard. Language: Kotlin. Choose a minimum SDK that Studio recommends (the default is fine for learning). - Click Finish and wait for Gradle to sync. The first sync downloads libraries, so use a good connection. It can be several hundred megabytes on a new installation.
Step 2: Find the files
In the Project panel (left side, switch the view to Android), open app > kotlin+java > com.techbase.birthdaycard. You will see:
com.techbase.birthdaycard
├── MainActivity.kt <- the entry point of your screen
└── ui
└── theme
├── Color.kt <- your colour palette
├── Theme.kt <- BirthdayCardTheme { ... }
└── Type.kt <- text styles (Typography)
Step 3: Read MainActivity.kt
The generated file looks roughly like this. Your Studio version may differ in small details, for example it may wrap the greeting in a Scaffold, but the idea is the same:
package com.techbase.birthdaycard
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.techbase.birthdaycard.ui.theme.BirthdayCardTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BirthdayCardTheme {
Greeting(name = "Android")
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
BirthdayCardTheme {
Greeting("Android")
}
}
Block by block:
| Piece | Meaning |
|---|---|
class MainActivity : ComponentActivity() |
An Activity is one screen-sized window of your app. ComponentActivity is the base class that supports Compose. Lesson 23 explains the Activity lifecycle. |
onCreate(...) |
Android calls this when the screen is created. Our setup goes here. |
super.onCreate(savedInstanceState) |
Lets the parent class do its own set-up first. Always keep it. |
enableEdgeToEdge() |
Lets the app draw behind the status bar and navigation bar, which is the modern Android look. Because of this you must sometimes add padding so your content is not hidden underneath them. We handle that below. |
setContent { ... } |
The bridge from Android to Compose. Everything inside the braces is composable. |
BirthdayCardTheme { ... } |
Applies the app's colours, typography and shapes to everything inside (lesson 22 covers theming). |
@Preview |
Marks a function that Android Studio should render in its Design pane. It is only for development; it does not ship in the app's behaviour. |
Run the app (the green triangle). You should see "Hello Android!" in the top-left corner of the screen. Congratulations: that is a Compose app.
Step 4: Meet the Preview pane
At the top right of the code editor are three buttons: Code, Split and Design. Choose Split. If the preview says it is out of date, click Build & Refresh (or the refresh icon). You should see the text "Hello Android!" on a white card inside the preview, without starting an emulator.
Edit the text in Greeting and look at the preview: after a short build it updates. Previews are the fastest feedback loop you have, so use them constantly.
A few preview rules:
- A
@Previewfunction must be@Composableand must take no parameters (unless you use a special advanced feature covered in lesson 44). showBackground = truepaints a background so white text would not vanish.- You can have several previews in a file to see different states.
- If the preview shows an error, read the message in the pane; it is usually a missing import or a build error elsewhere.
Text is the composable that shows words. The version to use is androidx.compose.material3.Text. You can pass only the text or add many styling parameters.
The most useful Text parameters
| Parameter | Example | Effect |
|---|---|---|
text |
"Hello" |
The words to show. |
fontSize |
32.sp |
Size of the letters. |
fontWeight |
FontWeight.Bold |
Thickness: Light, Normal, Medium, SemiBold, Bold. |
fontStyle |
FontStyle.Italic |
Slanted text. |
fontFamily |
FontFamily.Serif, Cursive, Monospace |
A built-in family of letter shapes. |
color |
Color(0xFF3E2723) |
Text colour. |
textAlign |
TextAlign.Center |
Alignment inside the width the Text has. |
lineHeight |
36.sp |
Distance between lines. |
letterSpacing |
2.sp |
Extra gap between letters. |
maxLines and overflow |
2, TextOverflow.Ellipsis |
Cut long text with "..." |
style |
MaterialTheme.typography.headlineMedium |
A bundle of settings from the theme. |
sp versus dp
Compose measures space with two units, and mixing them up is the number one beginner slip.
| Unit | Stands for | Used for | Scales with the user's font-size setting? |
|---|---|---|---|
dp |
density-independent pixels | Sizes of things: padding, widths, heights | No |
sp |
scale-independent pixels | Text size only | Yes |
A phone can have a tiny screen with a crowded display or a large one. A dp is built so that 16.dp looks about the same physical size on both. An sp is the same, but it also multiplies by the person's font size preference in the phone's Settings. A learner who struggles to read small text raises that setting; if you used dp for text, you would ignore their choice. That is why text always uses sp (lesson 47 returns to this under accessibility).
Both units need an import and the dot-suffix on a number:
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
Example: a styled greeting
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
@Composable
fun StyledGreeting() {
Text(
text = "Happy Birthday!",
fontSize = 40.sp,
fontWeight = FontWeight.Bold,
fontFamily = FontFamily.Serif,
color = Color(0xFF6A1B9A),
textAlign = TextAlign.Center,
lineHeight = 48.sp
)
}
@Preview(showBackground = true)
@Composable
fun StyledGreetingPreview() {
StyledGreeting()
}
What you should see in the preview: the words "Happy Birthday!" in bold, serif, purple letters at the top-left of the preview.
A few things to notice:
Color(0xFF6A1B9A)builds a colour from a hexadecimal number. The first two digits (FF) mean fully opaque, then red, green and blue pairs (6A,1B,9A). Import it fromandroidx.compose.ui.graphics.Color.textAlign = TextAlign.Centerhas no visible effect on a single short line, because theTextis only as wide as its words. Alignment works inside the width theTexthas. Give theTextmore width (for exampleModifier.fillMaxWidth()) and the centring becomes visible. This puzzles almost everyone the first time.- When two lines of large text wrap,
lineHeightkeeps them from touching or drifting apart. A good rule of thumb is a line height of roughly 1.1 to 1.3 times the font size for headings.
A screen holds more than one thing. Lesson 16 teaches layouts in full, but you need a small taste now.
Column, Row and Box in one minute
| Composable | Arranges children | Picture |
|---|---|---|
Column |
Top to bottom | Stack of plates |
Row |
Left to right | Children sitting on a bench |
Box |
On top of each other | Papers on a desk |
Column Row Box
+-------+ +---+---+---+ +---------+
| A | | A | B | C | | A (bg) |
| B | +---+---+---+ | B |
| C | +---------+
+-------+ B sits over A
Modifier: how you decorate and size a composable
A Modifier is a chain of instructions that changes how a composable looks or behaves: its size, padding, background, click behaviour and more. You build it by chaining calls with dots, and order matters.
Modifier
.fillMaxWidth() // take all the available width
.padding(16.dp) // then add 16dp of space around the content
Common modifiers:
| Modifier | Effect |
|---|---|
fillMaxSize(), fillMaxWidth(), fillMaxHeight() |
Fill the space the parent offers. |
padding(16.dp) |
Space inside the edge of this element (between its border and its content). |
size(48.dp), width(...), height(...) |
Exact sizes. |
background(Color) |
Paint a background colour. |
safeDrawingPadding() |
Add padding so content avoids the status bar, navigation bar and cut-outs. We use it because of enableEdgeToEdge(). |
A neat trick to remember the order rule: Modifier.padding(16.dp).background(Color.Red) puts the padding outside the red area (the red box is smaller). Modifier.background(Color.Red).padding(16.dp) paints the red first, then pads the content inside it. Try both in a preview to see it.
Alignment and Arrangement (preview)
Column has two parameters that place its children: verticalArrangement (how children are spaced along the column, up and down) and horizontalAlignment (where they sit across the column, left to right). For now remember that Arrangement.Center centres the group vertically and Alignment.CenterHorizontally centres it sideways. Lesson 16 explores this in depth.
We have been typing words directly into code. That works but has two problems: you cannot translate the app easily, and the same wording ends up copied in many places. Android keeps visible text in a resource file instead.
- Open
app > res > values > strings.xml. - Add your strings:
<resources>
<string name="app_name">Birthday Card</string>
<string name="happy_birthday_text">Happy Birthday %1$s!</string>
<string name="signature_text">From %1$s</string>
</resources>
%1$s is a placeholder: "the first argument, as a string". A translator can move it wherever their language needs. For example, a Yoruba or French version might put the name in a different spot in the sentence.
- Use them in Compose with
stringResource:
import androidx.compose.ui.res.stringResource
// inside a composable:
val message = stringResource(R.string.happy_birthday_text, "Ada")
val signature = stringResource(R.string.signature_text, "Tunde")
R is a class Android generates that holds an id for every resource. R.string.happy_birthday_text is that string's id. stringResource can only be called from a composable.
Tip: If
Rshows as red, you probably need to build the project (Build > Make Project) so Android generates it, or the file is in a different package from your namespace. If so, addimport com.techbase.birthdaycard.R.
Time to put it all together. We build in three small stages and look at the result of each.
Stage 1: The greeting composable
Create BirthdayGreeting: two texts in a column. Replace the contents of MainActivity.kt below the imports as shown in stage 3; first read the composable on its own:
@Composable
fun BirthdayGreeting(message: String, from: String, modifier: Modifier = Modifier) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier.padding(8.dp)
) {
Text(
text = message,
fontSize = 56.sp,
lineHeight = 64.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color(0xFF3E2723),
modifier = Modifier.fillMaxWidth()
)
Text(
text = from,
fontSize = 28.sp,
fontStyle = FontStyle.Italic,
color = Color(0xFF3E2723),
modifier = Modifier
.padding(16.dp)
.align(Alignment.End)
)
}
}
How it works:
- The function takes the two texts as parameters, so the card is reusable for anyone. It does not know it is for Ada.
- The
modifierparameter has a default ofModifier, so callers may ignore it. Inside we usemodifier.padding(8.dp), which means "whatever the caller asked for, plus 8dp of padding". Passing the caller's modifier to the outermost element is a convention to follow. Column(verticalArrangement = Arrangement.Center)centres both texts vertically when the column is tall (it will be, because the caller passesfillMaxSize).- The first
TextusesfillMaxWidth()soTextAlign.Centeris visible. - The signature uses
Modifier.align(Alignment.End), which only exists inside aColumn, and pushes it to the right-hand side. - Both texts set
colorexplicitly. See the common mistakes for why that matters.
Stage 2: The parent that supplies the background
In MainActivity, we wrap the greeting in a Surface that gives a warm background colour.
Stage 3: The complete file
Replace the whole of MainActivity.kt with this. It includes every import:
package com.techbase.birthdaycard
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.techbase.birthdaycard.ui.theme.BirthdayCardTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
BirthdayCardTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = Color(0xFFFFF3E0)
) {
BirthdayGreeting(
message = stringResource(R.string.happy_birthday_text, "Ada"),
from = stringResource(R.string.signature_text, "Tunde"),
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
)
}
}
}
}
}
@Composable
fun BirthdayGreeting(message: String, from: String, modifier: Modifier = Modifier) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier.padding(8.dp)
) {
Text(
text = message,
fontSize = 56.sp,
lineHeight = 64.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color(0xFF3E2723),
modifier = Modifier.fillMaxWidth()
)
Text(
text = from,
fontSize = 28.sp,
fontStyle = FontStyle.Italic,
color = Color(0xFF3E2723),
modifier = Modifier
.padding(16.dp)
.align(Alignment.End)
)
}
}
@Preview(showBackground = true, name = "Birthday card")
@Composable
fun BirthdayGreetingPreview() {
BirthdayCardTheme {
BirthdayGreeting(
message = "Happy Birthday Ada!",
from = "From Tunde",
modifier = Modifier.fillMaxSize()
)
}
}
What you should see when you run it: a warm cream-coloured screen. In the vertical middle, "Happy Birthday Ada!" appears large, bold and centred, probably wrapping onto two or three lines on a phone. Underneath it, on the right-hand side, is the italic signature "From Tunde". Nothing hides behind the status bar because of safeDrawingPadding().
The preview looks almost the same, except the background is white (the cream comes from the Surface that only exists in MainActivity). That is normal: a preview shows only the composable you call in it.
Understanding Surface
Surface is a Material composable that paints a background and sets the default text colour for its children. We gave it a custom color. Because that colour is not one of the theme's colours, Compose does not know what text colour suits it, so we set color on each Text ourselves. If we had used MaterialTheme.colorScheme.background, the text would follow the light or dark theme automatically.
The official course follows the birthday card with a Compose Article practice app: a title, a short paragraph and a long paragraph, with correct spacing and justified text. Here is our own version, an article about learning to code in Ibadan. Create a new Empty Activity project named Compose Article with package com.techbase.composearticle, and replace MainActivity.kt with:
package com.techbase.composearticle
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.techbase.composearticle.ui.theme.ComposeArticleTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
ComposeArticleTheme {
Surface(modifier = Modifier.fillMaxSize()) {
ArticleScreen(modifier = Modifier.safeDrawingPadding())
}
}
}
}
}
@Composable
fun ArticleScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(160.dp)
.background(Color(0xFF00695C)),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.banner_text),
color = Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.SemiBold
)
}
Text(
text = stringResource(R.string.article_title),
fontSize = 28.sp,
modifier = Modifier.padding(16.dp)
)
Text(
text = stringResource(R.string.article_intro),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(start = 16.dp, end = 16.dp)
)
Text(
text = stringResource(R.string.article_body),
textAlign = TextAlign.Justify,
modifier = Modifier.padding(16.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun ArticleScreenPreview() {
ComposeArticleTheme {
ArticleScreen()
}
}
and res/values/strings.xml:
<resources>
<string name="app_name">Compose Article</string>
<string name="banner_text">Techbase Academy, Ibadan</string>
<string name="article_title">Why Learn to Build Android Apps?</string>
<string name="article_intro">Most people in Nigeria meet the internet through a phone, so an app is often the shortest path between an idea and the people who need it.</string>
<string name="article_body">Building an app teaches you to break a big problem into small pieces, to test your ideas quickly, and to care about the person on the other side of the screen. You do not need an expensive computer to begin: a modest laptop, a free copy of Android Studio and steady practice are enough. Start with one small screen, make it work, and then add the next piece. Every professional developer began exactly this way.</string>
</resources>
What you should see: a teal banner across the top with white text centred inside it (Box with contentAlignment = Alignment.Center places its child in the middle). Below it, a title and two justified paragraphs with comfortable margins. verticalScroll lets the screen scroll when the text is longer than the display, which will happen on small phones or with a big font setting.
Why it works: Box stacks the banner text over the coloured rectangle. The start and end padding on the intro paragraph is applied with named arguments. padding(start = 16.dp, end = 16.dp) leaves the top and bottom untouched. The whole thing is a Column, so the pieces stack in order.
Exercise 1: Your own greeting card
Change the Birthday Card app so it becomes a graduation card for a friend of your choice. Requirements:
- Put the wording in
strings.xmlusing placeholders for the friend's name and your name. - Use a different
fontFamilyfor the main message. - Change the background colour of the
Surface. - Make the signature appear at the start (left) instead of the end.
- Write a second
@Previewthat shows a very long name, so you can check that the text still wraps nicely.
Add or change strings:
<resources>
<string name="app_name">Birthday Card</string>
<string name="graduation_text">Congratulations, %1$s!</string>
<string name="signature_text">From %1$s</string>
</resources>
Update the composable (imports unchanged except FontFamily):
import androidx.compose.ui.text.font.FontFamily
@Composable
fun GraduationGreeting(message: String, from: String, modifier: Modifier = Modifier) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier.padding(8.dp)
) {
Text(
text = message,
fontSize = 48.sp,
lineHeight = 56.sp,
fontFamily = FontFamily.Serif,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
color = Color(0xFF1B5E20),
modifier = Modifier.fillMaxWidth()
)
Text(
text = from,
fontSize = 24.sp,
color = Color(0xFF1B5E20),
modifier = Modifier
.padding(16.dp)
.align(Alignment.Start)
)
}
}
@Preview(showBackground = true)
@Composable
fun LongNamePreview() {
GraduationGreeting(
message = "Congratulations, Oluwaseun Adebayo-Johnson!",
from = "From Tunde",
modifier = Modifier.fillMaxSize()
)
}
In MainActivity, change the Surface colour to something like Color(0xFFE8F5E9) and call GraduationGreeting(message = stringResource(R.string.graduation_text, "Ada"), from = stringResource(R.string.signature_text, "Tunde"), modifier = Modifier.fillMaxSize().safeDrawingPadding()).
What you should see: a pale green screen with the serif, dark green message in the middle and the signature on the left. The long-name preview wraps the congratulation over several lines without cutting words off the screen.
Exercise 2: Fix the broken composable
This code has four problems. Find them before running.
import androidx.compose.material3.Text
fun welcomeMessage(name: String) {
Text(
text = "Welcome $name",
fontSize = 24,
color = "blue"
)
}
- The function is missing
@Composable. Without it, callingTextgives the error @Composable invocations can only happen from the context of a @Composable function. fontSize = 24is anInt. Text size needs aTextUnit, so write24.sp(and importandroidx.compose.ui.unit.sp).color = "blue"is aString. Colours areColorvalues:Color.BlueorColor(0xFF0000FF)(importandroidx.compose.ui.graphics.Color).- The name
welcomeMessagestarts with a lowercase letter. It works, but composables that emit UI should be named in PascalCase, so rename itWelcomeMessage.
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp
@Composable
fun WelcomeMessage(name: String) {
Text(
text = "Welcome $name",
fontSize = 24.sp,
color = Color.Blue
)
}
Exercise 3: A recipe card
Build a screen, with a @Preview, that shows a recipe card for jollof rice: a bold title, a line "Serves 4", a short paragraph of method text, and a footer line in small italic text aligned to the right. Put all the text in strings.xml, use a coloured Box as a header, and make the content scroll.
<resources>
<string name="app_name">Recipe Card</string>
<string name="recipe_title">Jollof Rice</string>
<string name="recipe_serves">Serves 4</string>
<string name="recipe_method">Fry blended pepper and tomato until the water dries and the oil rises. Add stock, seasoning and washed rice. Cover tightly and cook on low heat until the rice is soft.</string>
<string name="recipe_footer">Best eaten hot, with plantain.</string>
</resources>
package com.techbase.recipecard
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun RecipeCard(modifier: Modifier = Modifier) {
Column(modifier = modifier.verticalScroll(rememberScrollState())) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
.background(Color(0xFFE65100)),
contentAlignment = Alignment.CenterStart
) {
Text(
text = stringResource(R.string.recipe_title),
color = Color.White,
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(16.dp)
)
}
Text(
text = stringResource(R.string.recipe_serves),
fontSize = 18.sp,
modifier = Modifier.padding(16.dp)
)
Text(
text = stringResource(R.string.recipe_method),
modifier = Modifier.padding(horizontal = 16.dp)
)
Text(
text = stringResource(R.string.recipe_footer),
fontSize = 14.sp,
fontStyle = FontStyle.Italic,
modifier = Modifier
.padding(16.dp)
.align(Alignment.End)
)
}
}
@Preview(showBackground = true)
@Composable
fun RecipeCardPreview() {
RecipeCard()
}
The footer's align(Alignment.End) works because it sits directly inside a Column. Using Modifier.padding(horizontal = 16.dp) is a shortcut for the same padding on the left and right.
Create a new project and show your name, your town and one sentence about why you are learning Android. Style each line differently (size, weight, colour), keep the words in strings.xml, and use the Split view to check the result without running the emulator.
Mistake #1: Forgetting @Composable. The wrong code: a plain fun Greeting() { Text("Hi") } The problem: the compiler reports that @Composable invocations can only happen from a composable context. The fix: put @Composable on the line directly above fun.
Mistake #2: Typing a size without sp or dp. The wrong code: fontSize = 24 or padding(16). The problem: the number is an Int, not a unit, so the types do not match. The fix: 24.sp for text and 16.dp for spacing, with the matching imports from androidx.compose.ui.unit.
Mistake #3: Using dp for text size. The wrong code: fontSize = 20.dp The problem: a type error, but even if you convert it, text must scale with the user's font preference. The fix: sp for text, dp for everything else.
Mistake #4: Importing the wrong Text. The symptom: parameters are not recognised, or text ignores the theme. The fix: make sure the import is androidx.compose.material3.Text. Android Studio may offer several Text classes when you press Alt + Enter; pick the Material 3 one.
Mistake #5: Centred text that does not look centred. The wrong code: Text("Hi", textAlign = TextAlign.Center) on its own. The problem: the Text is only as wide as its words, so there is nothing to centre inside. The fix: add modifier = Modifier.fillMaxWidth(), or centre the whole composable with the parent's horizontalAlignment.
Mistake #6: Dark text on a dark background. The problem: you set a custom Surface colour and then your phone's dark mode switches the default text to near-white (or the opposite), making it unreadable. The fix: either use theme colours (MaterialTheme.colorScheme.background, which come with matching content colours) or set the color of your text explicitly. Test your screen in both light and dark mode (lesson 22 covers dark theme).
Mistake #7: Content hidden under the status bar. The cause: enableEdgeToEdge() lets your app draw behind the system bars. The fix: add Modifier.safeDrawingPadding(), statusBarsPadding(), or use a Scaffold (lesson 38) and apply its innerPadding.
Mistake #8: @Preview with parameters. The wrong code: @Preview @Composable fun P(name: String) { ... } The problem: previews cannot be given arguments this way. The fix: create a no-parameter preview function that calls your composable with sample values.
Mistake #9: Hard-coding long text in composables. The problem: translations are impossible and changes happen in many places. The fix: put text in strings.xml and use stringResource(R.string.name).
Mistake #10: Putting R.string.x directly into Text. The wrong code: Text(R.string.title) The problem: R.string.title is just an Int id, not the words. The fix: Text(stringResource(R.string.title)).
Answer each question in your head (or on paper), then reveal the answer.
Question 1. What does the @Composable annotation tell Android Studio and the compiler, and where can a composable function be called from?
It marks the function as one that describes part of the UI, so the Compose compiler can track it. A composable can only be called from another composable function or from the setContent { } block of an Activity.
Question 2. What is the difference between sp and dp, and which should you use for text?
Both are density-independent units. sp also scales with the user's font-size setting, so it is used for text. dp does not, and is used for sizes and spacing such as padding, width and height.
Question 3. What does @Preview do, and why is it useful?
It tells Android Studio to render that composable in the Design or Split pane, so you see your UI immediately without installing and launching the app on an emulator or phone. It speeds up development. A preview function must be @Composable and take no parameters.
Question 4. In Modifier.padding(16.dp).background(Color.Red), is the padding inside or outside the red area? What happens if you swap the two calls?
The padding is outside the red area, because it was applied first, so the red box is smaller. If you swap them (background(...) then padding(...)) the red is painted first and the padding is applied inside it, giving a larger red area with the content inset. Modifier order matters.
Question 5. Why should the visible words of an app live in strings.xml, and how do you read one in Compose?
It keeps wording in one place, lets the app be translated into other languages, and supports placeholders like %1$s. In Compose you read it with stringResource(R.string.the_name), adding arguments for placeholders, for example stringResource(R.string.happy_birthday_text, "Ada").
Go deeper: official resources
- Android Basics with Compose, Unit 1: Your first Android app: the official pathway for this lesson (birthday card and Compose Article).
- Jetpack Compose basics codelab: a longer walk-through of the same ideas.
- Jetpack Compose course page: the wider Compose curriculum.
- Android Studio user guide: tour of the IDE, including the Compose preview.
Project goal: Create a one-screen app that shows a poster for a real or imaginary community event, such as a church programme, a school open day or a coding class. It is a great first portfolio screenshot.
Requirements:
- Create a new Empty Activity project named
Event Posterwith packagecom.techbase.eventposter. - Write a composable
EventPoster(title, date, venue, details, modifier)that takes all the words as parameters. Do not put words directly in this composable. - Show the title large and bold, the date and venue in medium size, and the details as a paragraph with
TextAlign.Justify. - Use a coloured background with at least one
Boxand oneColumn. Choose text colours with enough contrast against the background. - Put all text in
strings.xmland read it withstringResource. - Use
Modifier.safeDrawingPadding()andverticalScrollso nothing is hidden or cut off. - Add a
@Previewand check it in the Split view.
Stretch goal: Add a second @Preview using a long event title, and a third where the text colour is changed for a dark poster. Check that all three look good.
<resources>
<string name="app_name">Event Poster</string>
<string name="event_title">Techbase Open Day</string>
<string name="event_date">Saturday, 12 December</string>
<string name="event_venue">Venue: Techbase Academy, Ibadan</string>
<string name="event_details">Come and see what our learners have built. There will be live app demonstrations, a short talk on how to start learning Android, and time to ask questions. Everyone is welcome, and no experience is needed.</string>
</resources>
package com.techbase.eventposter
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.techbase.eventposter.ui.theme.EventPosterTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
EventPosterTheme {
EventPoster(
title = stringResource(R.string.event_title),
date = stringResource(R.string.event_date),
venue = stringResource(R.string.event_venue),
details = stringResource(R.string.event_details),
modifier = Modifier.fillMaxSize()
)
}
}
}
}
@Composable
fun EventPoster(
title: String,
date: String,
venue: String,
details: String,
modifier: Modifier = Modifier,
textColor: Color = Color.White
) {
Box(modifier = modifier.background(Color(0xFF1A237E))) {
Column(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.verticalScroll(rememberScrollState())
.padding(24.dp)
) {
Text(
text = title,
color = textColor,
fontSize = 44.sp,
lineHeight = 52.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
Text(
text = date,
color = textColor,
fontSize = 22.sp,
modifier = Modifier.padding(bottom = 8.dp)
)
Text(
text = venue,
color = textColor,
fontSize = 22.sp,
modifier = Modifier.padding(bottom = 24.dp)
)
Text(
text = details,
color = textColor,
fontSize = 18.sp,
lineHeight = 26.sp,
textAlign = TextAlign.Justify,
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Preview(showBackground = true)
@Composable
fun EventPosterPreview() {
EventPosterTheme {
EventPoster(
title = "Techbase Open Day",
date = "Saturday, 12 December",
venue = "Venue: Techbase Academy, Ibadan",
details = "Come and see what our learners have built.",
modifier = Modifier.fillMaxSize()
)
}
}
@Preview(showBackground = true, name = "Long title")
@Composable
fun EventPosterLongTitlePreview() {
EventPosterTheme {
EventPoster(
title = "The Annual Techbase Community Programming Showcase and Open Day",
date = "Saturday, 12 December",
venue = "Venue: Techbase Academy, Ibadan",
details = "Come and see what our learners have built.",
modifier = Modifier.fillMaxSize()
)
}
}
What you should see: a deep blue poster with a large bold white title, the date and venue beneath it, and a justified paragraph of details. In the long-title preview the title wraps over several lines and nothing is cut off, and on a small phone the whole poster scrolls. The text colour is white on dark blue, which has strong contrast. The textColor parameter has a default so you can change it for other designs without editing the body of the composable.
Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.
Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 15 project”.
On github.com click New repository, name it android-lesson-15-introduction-to-jetpack-compose-and-text-c, set it to Public, leave Add a README unticked, and click Create repository.
Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.
Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉
Lesson 15 complete! 🎉
You covered:
- Lesson Introduction
- Prerequisite Concepts
- Conceptual Understanding: Declarative UI and Composable Functions
- Step-by-Step: Create the Project and Read Its Code
- Step-by-Step: Making Text Look the Way You Want
- Step-by-Step: Arranging Composables with Column, Box and Modifier
- Step-by-Step: Strings in `strings.xml`
- Step-by-Step Build: The Birthday Card App
- Step-by-Step Build: The Compose Article App
- Guided Practice Exercises
- Common Beginner Mistakes (and How to Fix Them)
- Knowledge Check