Composables

Core Jetpack Compose composables — Text, Image, TextField, LazyColumn, Scaffold, and common UI patterns

Composables are the building blocks of Compose UI. Here’s a reference for the most commonly used composables.

Text

// Basic
Text("Hello, World!")

// Styled
Text(
    text = "Hello, World!",
    style = MaterialTheme.typography.headlineMedium,
    color = MaterialTheme.colorScheme.primary,
)

// Rich text with AnnotatedString
Text(
    buildAnnotatedString {
        withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
            append("Hello")
        }
        append(", ")
        withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
            append("World")
        }
        append("!")
    }
)

// Clickable text
ClickableText(
    text = AnnotatedString("Terms and Conditions"),
    onClick = { offset -> /* handle click */ },
)

// Overflow
Text(
    text = longText,
    maxLines = 2,
    overflow = TextOverflow.Ellipsis,
)

TextField

// Basic text field
var text by remember { mutableStateOf("") }
TextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("Enter text") },
)

// Outlined text field (M3 default)
OutlinedTextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("Email") },
    leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) },
    keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
    singleLine = true,
)

// Password field
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(
                if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
                contentDescription = if (passwordVisible) "Hide password" else "Show password"
            )
        }
    },
)

// Error state
OutlinedTextField(
    value = email,
    onValueChange = { email = it },
    label = { Text("Email") },
    isError = !isValidEmail,
    supportingText = { if (!isValidEmail) Text("Invalid email address") },
)

Button variants

// Filled (primary)
Button(onClick = { /* ... */ }) { Text("Save") }

// Elevated
ElevatedButton(onClick = { /* ... */ }) { Text("Edit") }

// Outlined
OutlinedButton(onClick = { /* ... */ }) { Text("Cancel") }

// Text
TextButton(onClick = { /* ... */ }) { Text("Learn more") }

// Icon button
IconButton(onClick = { /* ... */ }) {
    Icon(Icons.Default.Favorite, contentDescription = "Favorite")
}

// Filled tonal
FilledTonalButton(onClick = { /* ... */ }) { Text("Secondary action") }

// With icon
Button(onClick = { /* ... */ }) {
    Icon(Icons.Default.Save, contentDescription = null)
    Spacer(Modifier.width(8.dp))
    Text("Save")
}

Image

// Resource image
Image(
    painter = painterResource(R.drawable.my_image),
    contentDescription = "My image",
    modifier = Modifier.size(120.dp),
    contentScale = ContentScale.Crop,
)

// URL image (with Coil)
AsyncImage(
    model = "https://example.com/photo.jpg",
    contentDescription = "Photo",
    modifier = Modifier.size(120.dp).clip(CircleShape),
    contentScale = ContentScale.Crop,
)

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

LazyColumn / LazyRow

// Vertical list
LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
) {
    // Static items
    item { Header(text = "My List") }

    // Dynamic items
    items(users, key = { it.id }) { user ->
        UserRow(user = user)
    }

    // Indexed items
    itemsIndexed(items) { index, item ->
        Text("Item $index: $item")
    }

    item { Footer() }
}

// Horizontal list
LazyRow(
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    contentPadding = PaddingValues(horizontal = 16.dp),
) {
    items(categories) { category ->
        CategoryChip(category)
    }
}

// Grid (LazyVerticalGrid)
LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    contentPadding = PaddingValues(8.dp),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
) {
    items(photos) { photo ->
        PhotoCard(photo)
    }
}

Scaffold

@Composable
fun AppScreen() {
    val snackbarHostState = remember { SnackbarHostState() }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("My App") },
                navigationIcon = {
                    IconButton(onClick = { navController.popBackStack() }) {
                        Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back")
                    }
                },
            )
        },
        bottomBar = {
            NavigationBar {
                NavigationBarItem(
                    selected = currentRoute == "home",
                    onClick = { navController.navigate("home") },
                    icon = { Icon(Icons.Default.Home, "Home") },
                    label = { Text("Home") },
                )
                NavigationBarItem(
                    selected = currentRoute == "profile",
                    onClick = { navController.navigate("profile") },
                    icon = { Icon(Icons.Default.Person, "Profile") },
                    label = { Text("Profile") },
                )
            }
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { /* add */ }) {
                Icon(Icons.Default.Add, "Add")
            }
        },
        snackbarHost = { SnackbarHost(snackbarHostState) },
    ) { innerPadding ->
        Column(modifier = Modifier.padding(innerPadding)) {
            // Screen content
        }
    }
}

Pull-to-refresh

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RefreshableList(viewModel: MyViewModel) {
    val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = { viewModel.refresh() },
    ) {
        LazyColumn {
            items(viewModel.items) { item ->
                ItemRow(item)
            }
        }
    }
}

Switch, Checkbox, Slider, Progress

// Switch
var enabled by remember { mutableStateOf(true) }
Switch(checked = enabled, onCheckedChange = { enabled = it })

// Checkbox
var checked by remember { mutableStateOf(false) }
Checkbox(checked = checked, onCheckedChange = { checked = it })

// Tri-state checkbox
val state = remember { mutableStateOf(ToggleableState.Indeterminate) }
TriStateCheckbox(state = state.value, onClick = { /* cycle state */ })

// Slider
var value by remember { mutableStateOf(0.5f) }
Slider(value = value, onValueChange = { value = it })

// Linear progress
LinearProgressIndicator(progress = { 0.7f })

// Indeterminate
LinearProgressIndicator()

// Circular progress
CircularProgressIndicator(progress = { 0.7f })
CircularProgressIndicator() // indeterminate