Part of Github Copilot with Kotlin

Jetpack Compose: Create a button with a callback

Sandy LaneSandy Lane

Video: Jetpack Compose: Create a button with a callback by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Jetpack Compose: Button with a Callback

Button(onClick = { ... }) { Text("Click me") }. Pass the click handler as a parameter for testability and reuse. The simplest interactive Composable.

A small Compose puzzle: build a button that runs a function when clicked. The pattern is foundational — every interactive widget in Compose follows the same shape.

The Copilot prompt

import androidx.compose.runtime.Composable

// A button that triggers a callback when clicked
@Composable
fun GreetingButton(onClickAction: () -> Unit) {

Copilot generates:

import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun GreetingButton(onClickAction: () -> Unit) {
  Button(onClick = { onClickAction() }) {
    Text("Click me")
  }
}

Walkthrough

1. The function signature. @Composable fun GreetingButton(onClickAction: () -> Unit). Composable functions are marked with @Composable — they can read state and create UI.

onClickAction: () -> Unit is a function parameter — a lambda taking no args, returning nothing. The caller provides whatever should happen on click.

2. Button(onClick = ..., content = ...). Material's button takes an onClick callback and a content slot. Hitting the button triggers the callback.

3. Text("Click me"). The button's label. Inside the button's content lambda, you can put any Composables — text, icons, rows, etc.

Calling the button

@Composable
fun MyScreen() {
  GreetingButton(onClickAction = {
    println("Button clicked!")
  })
}

The caller controls what happens. The button itself is dumb — it just exposes a click event.

Why hoist the callback?

Compare two designs:

// Bad: hardcoded action
@Composable
fun GreetingButton() {
  Button(onClick = {
    println("Hello!")
  }) {
    Text("Click me")
  }
}

// Good: action passed in
@Composable
fun GreetingButton(onClickAction: () -> Unit) {
  Button(onClick = onClickAction) {
    Text("Click me")
  }
}

The hoisted version: - Reusable. Same button can do different things in different contexts. - Testable. UI tests can pass a mock onClickAction and verify it was called. - Composable up the tree. State and logic stay in the parent / ViewModel.

This is state hoisting applied to events. The Composable doesn't know what to do; the parent does.

The Material 3 version

androidx.compose.material.Button is Material 2. Modern code uses Material 3:

import androidx.compose.material3.Button
import androidx.compose.material3.Text

@Composable
fun GreetingButton(onClickAction: () -> Unit) {
  Button(onClick = onClickAction) {
    Text("Click me")
  }
}

The Material 3 button has rounded corners, subtle shadow, primary color from the theme. Same API.

Customizing appearance

import androidx.compose.material3.ButtonDefaults
import androidx.compose.ui.graphics.Color

@Composable
fun GreetingButton(onClickAction: () -> Unit) {
  Button(
    onClick = onClickAction,
    colors = ButtonDefaults.buttonColors(
      containerColor = Color.Magenta,
      contentColor = Color.White,
    ),
  ) {
    Text("Click me", style = MaterialTheme.typography.titleMedium)
  }
}

ButtonDefaults provides factories for the various color/elevation/shape configurations. For brand-specific styling, override containerColor and contentColor.

Other button types

OutlinedButton(onClick = ...) { Text("Outlined") }   // border, no fill
TextButton(onClick = ...) { Text("Text") }            // no border, no fill — flat
FilledTonalButton(onClick = ...) { Text("Tonal") }    // mid-emphasis, tinted
IconButton(onClick = ...) { Icon(Icons.Default.Close, "Close") }

Material 3 has a hierarchy of button styles for different emphasis levels:

  • Filled (Button) — primary action, highest emphasis.
  • Filled tonal — secondary action.
  • Outlined — alternative or cancel.
  • Text — low-emphasis, often inline.
  • Icon — for icon-only buttons.

Use the right level — too many Buttons on a screen looks busy.

Disabled state

Button(
  onClick = onClickAction,
  enabled = isLoading == false,
) { Text("Submit") }

enabled = false greys the button out and ignores clicks. Common pattern: disable during async work.

With a data parameter

@Composable
fun DeleteButton(itemId: Int, onDelete: (Int) -> Unit) {
  Button(onClick = { onDelete(itemId) }) {
    Text("Delete item $itemId")
  }
}

The callback now takes the ID. Caller wires it up:

DeleteButton(itemId = 42, onDelete = { id -> viewModel.delete(id) })

Common mistakes

Forgetting @Composable. Function compiles, but using Composable APIs inside (like Button) errors. Every Composable function needs the annotation.

Doing work in the Composable body. A Composable runs on every recomposition. Side effects belong in LaunchedEffect, callbacks, or hoisted up. The body should be (mostly) pure UI declaration.

Hardcoding the callback. Couples the button to a specific action. Hoist via parameter for reuse.

Using Button.onClick = { someState++ }. Mutating local state directly is fine for tiny demos, but real apps hoist state to a ViewModel.

Wrong Material library. Mixing androidx.compose.material.* (Material 2) and androidx.compose.material3.* (Material 3) in the same screen produces inconsistent styling. Pick one.

What's next

Episode 16: A button that shows when it was clicked. Adds local state — the button toggles a flag and updates its own label.

Recap

@Composable fun MyButton(onClick: () -> Unit) — pass the click handler as a parameter for reuse and testability. Button(onClick = ...) from Material 3 (or 2). Use the hierarchy: Button (filled), FilledTonalButton, OutlinedButton, TextButton, IconButton. Customize via ButtonDefaults.buttonColors(...). State (clicked state, loading) hoists up via parameters; event handlers come down the same way.

Next episode: button with click state.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.