You know layouts. You know modifiers. Now let’s learn the components you will use in every single screen — Text, Button, Image, and TextField.

These are the building blocks of every Android app. Get comfortable with them and you can build almost anything.

Text — Showing Words on Screen

You have already used Text, but there is much more to it.

Basic Text

Text("Hello, World!")

Styling Text

Text(
    text = "Styled Text",
    fontSize = 24.sp,
    fontWeight = FontWeight.Bold,
    color = Color.Blue,
    letterSpacing = 1.sp,
    textAlign = TextAlign.Center,
    modifier = Modifier.fillMaxWidth()
)

All the Style Options

PropertyWhat It DoesExample
fontSizeText size16.sp, 24.sp
fontWeightThicknessFontWeight.Bold, FontWeight.Light
fontStyleItalic or normalFontStyle.Italic
colorText colorColor.Red, Color(0xFF333333)
textAlignAlignmentTextAlign.Center, TextAlign.End
letterSpacingSpace between letters2.sp
lineHeightSpace between lines28.sp
maxLinesMaximum number of lines1, 3
overflowWhat happens when text is too longTextOverflow.Ellipsis
textDecorationUnderline or strikethroughTextDecoration.Underline

Truncating Long Text

When text is too long for the available space:

Text(
    text = "This is a very long text that will not fit on one line and needs to be truncated",
    maxLines = 1,
    overflow = TextOverflow.Ellipsis
)

This shows: “This is a very long text that will not fit on one li…” with three dots at the end.

Multiple Styles in One Text

Use buildAnnotatedString to mix styles:

Text(
    buildAnnotatedString {
        append("Hello, ")
        withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = Color.Blue)) {
            append("World")
        }
        append("! Welcome to ")
        withStyle(SpanStyle(fontStyle = FontStyle.Italic)) {
            append("Compose")
        }
    }
)

This shows “Hello, World! Welcome to Compose” — different styles in one text.

Selectable Text

By default, users cannot select and copy text. To allow it:

SelectionContainer {
    Text("You can select and copy this text")
}

Button — Handling User Taps

Basic Button

Button(onClick = { println("Clicked!") }) {
    Text("Click Me")
}

Every Button needs two things: an onClick function and content inside the curly braces.

Button Types

Compose has several button styles:

// Filled button (primary action)
Button(onClick = {}) {
    Text("Primary")
}

// Outlined button (secondary action)
OutlinedButton(onClick = {}) {
    Text("Secondary")
}

// Text button (low emphasis)
TextButton(onClick = {}) {
    Text("Tertiary")
}

// Filled tonal button (medium emphasis)
FilledTonalButton(onClick = {}) {
    Text("Tonal")
}

// Elevated button (with shadow)
ElevatedButton(onClick = {}) {
    Text("Elevated")
}

Button with Icon

Button(onClick = {}) {
    Icon(
        imageVector = Icons.Default.Add,
        contentDescription = "Add",
        modifier = Modifier.size(18.dp)
    )
    Spacer(modifier = Modifier.width(8.dp))
    Text("Add Item")
}

Icon Button

For buttons that are just an icon (like a toolbar):

IconButton(onClick = {}) {
    Icon(
        imageVector = Icons.Default.Favorite,
        contentDescription = "Like"
    )
}

Floating Action Button

The round button that floats at the bottom of the screen:

FloatingActionButton(onClick = {}) {
    Icon(Icons.Default.Add, contentDescription = "Add")
}

Full Width Button

Button(
    onClick = {},
    modifier = Modifier.fillMaxWidth(),
    shape = RoundedCornerShape(12.dp)
) {
    Text("Submit", modifier = Modifier.padding(vertical = 4.dp))
}

Disabled Button

Button(
    onClick = {},
    enabled = false  // Grayed out, cannot be clicked
) {
    Text("Not Available")
}

Custom Button Colors

Button(
    onClick = {},
    colors = ButtonDefaults.buttonColors(
        containerColor = Color(0xFF6200EE),
        contentColor = Color.White
    )
) {
    Text("Custom Color")
}

Image — Showing Pictures

Image from Resources

Put your image in res/drawable/, then:

Image(
    painter = painterResource(R.drawable.my_photo),
    contentDescription = "A photo",
    modifier = Modifier.size(200.dp)
)

Always provide contentDescription for accessibility. If the image is decorative and does not add information, use null:

Image(
    painter = painterResource(R.drawable.background),
    contentDescription = null  // Decorative only
)

Content Scale

How the image fits inside its container:

Image(
    painter = painterResource(R.drawable.photo),
    contentDescription = "Photo",
    modifier = Modifier
        .fillMaxWidth()
        .height(200.dp),
    contentScale = ContentScale.Crop  // Fill and crop
)
ContentScaleWhat It Does
CropFill the container, crop what doesn’t fit
FitShow the full image, may leave empty space
FillBoundsStretch to fill (may distort)
FillWidthFill the width, scale height proportionally
FillHeightFill the height, scale width proportionally
InsideShow at original size or smaller
NoneShow at original size

Circular Image

Image(
    painter = painterResource(R.drawable.profile),
    contentDescription = "Profile picture",
    modifier = Modifier
        .size(80.dp)
        .clip(CircleShape)
        .border(2.dp, MaterialTheme.colorScheme.primary, CircleShape),
    contentScale = ContentScale.Crop
)

Loading Images from the Internet

Compose does not load network images by default. You need a library. The most popular one is Coil:

Add to your build.gradle:

implementation("io.coil-kt:coil-compose:2.6.0")

Then use AsyncImage:

AsyncImage(
    model = "https://example.com/photo.jpg",
    contentDescription = "User photo",
    modifier = Modifier
        .size(100.dp)
        .clip(CircleShape),
    contentScale = ContentScale.Crop
)

Coil handles caching, placeholders, and error states:

AsyncImage(
    model = "https://example.com/photo.jpg",
    contentDescription = "Photo",
    modifier = Modifier.size(200.dp),
    placeholder = painterResource(R.drawable.placeholder),
    error = painterResource(R.drawable.error_image)
)

Icon

For simple icons, use the built-in Material Icons:

Icon(
    imageVector = Icons.Default.Home,
    contentDescription = "Home",
    tint = MaterialTheme.colorScheme.primary,
    modifier = Modifier.size(24.dp)
)

Common icons: Icons.Default.Home, Icons.Default.Search, Icons.Default.Settings, Icons.Default.Person, Icons.Default.Add, Icons.Default.Delete, Icons.Default.ArrowBack, Icons.Default.Favorite.

TextField — Getting User Input

Basic TextField

var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("Enter your name") }
)

Two things are required:

  • value — the current text (comes from state)
  • onValueChange — what happens when the user types (updates the state)

OutlinedTextField

The outlined version looks cleaner and is more common in modern apps:

var email by remember { mutableStateOf("") }

OutlinedTextField(
    value = email,
    onValueChange = { email = it },
    label = { Text("Email") },
    modifier = Modifier.fillMaxWidth()
)

TextField with Icon

OutlinedTextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("Search") },
    leadingIcon = {
        Icon(Icons.Default.Search, contentDescription = "Search")
    },
    modifier = Modifier.fillMaxWidth()
)

You can also add a trailingIcon (on the right side):

OutlinedTextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("Search") },
    trailingIcon = {
        if (text.isNotEmpty()) {
            IconButton(onClick = { text = "" }) {
                Icon(Icons.Default.Clear, contentDescription = "Clear")
            }
        }
    }
)

Password TextField

var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }

OutlinedTextField(
    value = password,
    onValueChange = { password = it },
    label = { Text("Password") },
    visualTransformation = if (passwordVisible) {
        VisualTransformation.None
    } else {
        PasswordVisualTransformation()
    },
    trailingIcon = {
        IconButton(onClick = { passwordVisible = !passwordVisible }) {
            Icon(
                imageVector = if (passwordVisible) {
                    Icons.Default.Visibility
                } else {
                    Icons.Default.VisibilityOff
                },
                contentDescription = if (passwordVisible) "Hide password" else "Show password"
            )
        }
    },
    modifier = Modifier.fillMaxWidth()
)

Keyboard Options

Control what keyboard shows up:

// Email keyboard
OutlinedTextField(
    value = email,
    onValueChange = { email = it },
    keyboardOptions = KeyboardOptions(
        keyboardType = KeyboardType.Email,
        imeAction = ImeAction.Next
    )
)

// Number keyboard
OutlinedTextField(
    value = age,
    onValueChange = { age = it },
    keyboardOptions = KeyboardOptions(
        keyboardType = KeyboardType.Number,
        imeAction = ImeAction.Done
    )
)
KeyboardTypeShows
TextNormal keyboard
NumberNumber pad
PhonePhone dialer layout
EmailKeyboard with @ symbol
PasswordText with obscured input
UriKeyboard with / and .com
ImeActionButton on keyboard
NextMoves to next field
DoneCloses keyboard
SearchSearch icon
SendSend icon
GoGo icon

Error State

var email by remember { mutableStateOf("") }
var isError by remember { mutableStateOf(false) }

OutlinedTextField(
    value = email,
    onValueChange = {
        email = it
        isError = !it.contains("@")
    },
    label = { Text("Email") },
    isError = isError,
    supportingText = {
        if (isError) {
            Text("Please enter a valid email")
        }
    },
    modifier = Modifier.fillMaxWidth()
)

Practical Example: Login Form

Let’s combine everything into a real login screen:

@Composable
fun LoginScreen() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var passwordVisible by remember { mutableStateOf(false) }
    var isLoading by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        // Title
        Text(
            text = "Welcome Back",
            fontSize = 28.sp,
            fontWeight = FontWeight.Bold
        )

        Text(
            text = "Sign in to continue",
            fontSize = 16.sp,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )

        Spacer(modifier = Modifier.height(32.dp))

        // Email field
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            leadingIcon = {
                Icon(Icons.Default.Email, contentDescription = null)
            },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Email,
                imeAction = ImeAction.Next
            ),
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))

        // Password field
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            leadingIcon = {
                Icon(Icons.Default.Lock, contentDescription = null)
            },
            trailingIcon = {
                IconButton(onClick = { passwordVisible = !passwordVisible }) {
                    Icon(
                        imageVector = if (passwordVisible) {
                            Icons.Default.Visibility
                        } else {
                            Icons.Default.VisibilityOff
                        },
                        contentDescription = null
                    )
                }
            },
            visualTransformation = if (passwordVisible) {
                VisualTransformation.None
            } else {
                PasswordVisualTransformation()
            },
            keyboardOptions = KeyboardOptions(
                keyboardType = KeyboardType.Password,
                imeAction = ImeAction.Done
            ),
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(8.dp))

        // Forgot password
        TextButton(
            onClick = { /* Navigate to reset */ },
            modifier = Modifier.align(Alignment.End)
        ) {
            Text("Forgot password?")
        }

        Spacer(modifier = Modifier.height(24.dp))

        // Login button
        Button(
            onClick = { isLoading = true },
            enabled = email.isNotBlank() && password.isNotBlank() && !isLoading,
            modifier = Modifier.fillMaxWidth(),
            shape = RoundedCornerShape(12.dp)
        ) {
            if (isLoading) {
                CircularProgressIndicator(
                    modifier = Modifier.size(20.dp),
                    color = MaterialTheme.colorScheme.onPrimary,
                    strokeWidth = 2.dp
                )
            } else {
                Text(
                    "Sign In",
                    modifier = Modifier.padding(vertical = 4.dp)
                )
            }
        }

        Spacer(modifier = Modifier.height(16.dp))

        // Register link
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.Center
        ) {
            Text("Don't have an account? ")
            TextButton(onClick = { /* Navigate to register */ }) {
                Text("Sign Up")
            }
        }
    }
}

This login form has:

  • Email field with email keyboard
  • Password field with show/hide toggle
  • Forgot password link
  • Login button that disables when fields are empty
  • Loading spinner when submitting
  • Sign up link at the bottom

All from the components we covered in this tutorial.

Quick Summary

ComponentWhat It DoesKey Properties
TextShows textfontSize, fontWeight, color, maxLines
ButtonClickable buttononClick, enabled, colors
OutlinedButtonButton with borderSame as Button
IconButtonIcon-only buttononClick
ImageShows imagespainter, contentScale, contentDescription
AsyncImageLoads images from URLNeeds Coil library
IconMaterial iconimageVector, tint
TextFieldText inputvalue, onValueChange, label
OutlinedTextFieldOutlined text inputSame as TextField

Result

Here is what the app looks like when you run the code from this tutorial:

Light ModeDark Mode
Tutorial 4 LightTutorial 4 Dark

Source Code

The complete working code for this tutorial is on GitHub:

View source code on GitHub →

What’s Next?

In the next tutorial, we will learn about State — the most important concept in Jetpack Compose. How does the login button know to disable when fields are empty? How does the password toggle work? That is all state management, and understanding it is the key to building any real app.

See you there.