Jetpack Compose has revolutionized how Android developers build user interfaces. As Google's modern toolkit for building native UI, Compose uses a declarative approach that simplifies and accelerates UI development. At the heart of this system are Compose widgets - the building blocks that enable developers to create everything from simple text displays to complex, interactive components.
In this comprehensive guide, we'll explore the extensive world of Jetpack Compose widgets, examining their functionality, implementation, and best practices. Whether you're just starting with Compose or looking to deepen your understanding, this article will provide valuable insights into creating elegant, responsive, and maintainable user interfaces with Jetpack Compose.
Before diving into specific widgets, it's essential to understand the core principles that make Jetpack Compose different from the traditional View-based system.
Jetpack Compose employs a declarative approach to UI building. Unlike the imperative approach of the View system, where you manually update the UI in response to state changes, Compose automatically updates the UI when the state changes. This paradigm shift significantly reduces boilerplate code and potential bugs related to UI state management.
@Composable
fun GreetingWidget(name: String) {
Text("Hello, $name!")
}
This simple example demonstrates how Compose widgets are created using functions annotated with @Composable
. The function describes what the UI should look like based on the current state rather than how to update it.
In Jetpack Compose, the UI is created through composition - the process of calling Composable functions to build a UI hierarchy. When state changes, Compose performs recomposition, efficiently updating only the parts of the UI affected by the state change. This smart redrawing mechanism is a key advantage of Compose, making UI updates more efficient and predictable.
State is central to Compose's functionality. The toolkit provides several ways to manage state:
@Composable
fun CounterWidget() {
var count by remember { mutableStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
In this example, mutableStateOf()
creates a state holder object while remember
preserves the state across recompositions. When the state changes, only the affected parts of the UI are recomposed.
Text is fundamental to almost any user interface. Jetpack Compose provides robust text widgets with extensive customization options.
The Text
composable is the primary way to display text in Compose:
@Composable
fun TextExamples() {
Column {
Text("Basic Text")
Text(
text = "Styled Text",
color = Color.Blue,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "This is a very long text that will demonstrate how overflow is handled in Jetpack Compose Text widget with ellipsis at the end",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
The Text widget offers extensive customization through parameters like color
, fontSize
, fontWeight
, and textAlign
. You can also control text overflow behavior with parameters like maxLines
and overflow
.
For text input, Compose offers the TextField
and OutlinedTextField
composables:
@Composable
fun TextFieldExample() {
var text by remember { mutableStateOf("") }
Column {
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Enter text") },
placeholder = { Text("Placeholder") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("Outlined TextField") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done
),
modifier = Modifier.fillMaxWidth()
)
}
}
TextField widgets handle user input and provide properties for customizing the input experience, such as:
Layout widgets in Jetpack Compose determine how UI elements are arranged on the screen. These foundational widgets provide the structure for your application's interface.
Row and Column are the primary layout widgets in Compose:
@Composable
fun RowColumnExample() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("Column Example", fontWeight = FontWeight.Bold)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Red)
)
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Green)
)
Box(
modifier = Modifier
.size(50.dp)
.background(Color.Blue)
)
}
}
}
Row
arranges items horizontally, while Column
arranges them vertically. Both offer parameters like horizontalArrangement
/verticalArrangement
and horizontalAlignment
/verticalAlignment
to control how items are positioned and aligned.
The Box
composable allows elements to be stacked on top of each other:
@Composable
fun BoxExample() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.LightGray)
) {
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.align(Alignment.TopStart)
)
Text(
text = "Centered Text",
modifier = Modifier.align(Alignment.Center),
color = Color.White
)
Button(
onClick = { /* Action */ },
modifier = Modifier.align(Alignment.BottomEnd)
) {
Text("Button")
}
}
}
Box is particularly useful for overlaying content, creating custom UI components, or positioning elements at specific locations within a container.
For more complex layouts, Compose offers ConstraintLayout
:
@Composable
fun ConstraintLayoutExample() {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(16.dp)
) {
val (text, button, image) = createRefs()
Text(
text = "Constrained Text",
modifier = Modifier.constrainAs(text) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)
Button(
onClick = { /* Action */ },
modifier = Modifier.constrainAs(button) {
bottom.linkTo(parent.bottom)
end.linkTo(parent.end)
}
) {
Text("Button")
}
Box(
modifier = Modifier
.size(60.dp)
.background(Color.Gray)
.constrainAs(image) {
centerTo(parent)
}
)
}
}
ConstraintLayout enables precise control over positioning through constraints, similar to ConstraintLayout in the View system. It's particularly valuable for complex layouts that can't be easily achieved with simpler layout widgets.
Interactive widgets handle user input and provide visual feedback, forming the basis of user interaction in your Jetpack Compose applications.
Compose offers several button variants to suit different design needs:
@Composable
fun ButtonExamples() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(onClick = { /* Action */ }) {
Text("Standard Button")
}
OutlinedButton(onClick = { /* Action */ }) {
Text("Outlined Button")
}
TextButton(onClick = { /* Action */ }) {
Text("Text Button")
}
IconButton(onClick = { /* Action */ }) {
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "Favorite"
)
}
FloatingActionButton(onClick = { /* Action */ }) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add"
)
}
ExtendedFloatingActionButton(
text = { Text("Extended FAB") },
icon = { Icon(Icons.Default.Add, contentDescription = null) },
onClick = { /* Action */ }
)
}
}
Each button type serves different UI patterns:
Button
for primary actionsOutlinedButton
for secondary actionsTextButton
for low-emphasis actionsIconButton
for icon-only actionsFloatingActionButton
and ExtendedFloatingActionButton
for promoted actionsCompose provides several widgets for selection tasks:
@Composable
fun SelectionWidgetsExample() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
var checked by remember { mutableStateOf(false) }
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = checked,
onCheckedChange = { checked = it }
)
Spacer(Modifier.width(8.dp))
Text("Checkbox Example")
}
var switchState by remember { mutableStateOf(false) }
Row(verticalAlignment = Alignment.CenterVertically) {
Switch(
checked = switchState,
onCheckedChange = { switchState = it }
)
Spacer(Modifier.width(8.dp))
Text("Switch Example")
}
var selected by remember { mutableStateOf(false) }
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = selected,
onClick = { selected = true }
)
Spacer(Modifier.width(8.dp))
Text("Radio Button Example")
}
var sliderValue by remember { mutableStateOf(0f) }
Text("Slider Value: ${(sliderValue * 100).toInt()}")
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
valueRange = 0f..1f
)
}
}
These selection widgets allow users to choose from options, toggle states, or select values along a continuous range.
Lists are essential for displaying collections of items. Jetpack Compose provides efficient list widgets optimized for performance.
LazyColumn
and LazyRow
are Compose's answer to RecyclerView:
@Composable
fun LazyListExample() {
val items = List(100) { "Item ${it + 1}" }
Column {
Text(
text = "LazyColumn Example",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(16.dp)
)
LazyColumn {
items(items) { item ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
elevation = 4.dp
) {
Text(
text = item,
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
These lazy list widgets render only the visible items, efficiently handling large data sets. The items
DSL function simplifies displaying collections of data.
For grid layouts, Compose offers LazyVerticalGrid
:
@Composable
fun LazyGridExample() {
val items = List(100) { "Item ${it + 1}" }
Column {
Text(
text = "LazyVerticalGrid Example",
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(16.dp)
)
LazyVerticalGrid(
columns = GridCells.Fixed(2),
contentPadding = PaddingValues(8.dp),
modifier = Modifier.fillMaxWidth()
) {
items(items) { item ->
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
elevation = 4.dp
) {
Text(
text = item,
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
}
}
}
}
LazyVerticalGrid
arranges items in a grid pattern with specified column configurations through GridCells
.
Container widgets in Jetpack Compose provide structure and visual styling to your content.
The Card
composable creates a surface with elevation and rounded corners:
@Composable
fun CardExample() {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = 8.dp,
shape = RoundedCornerShape(16.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Card Title",
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "This is the content of the card. Cards are useful for containing related information and actions about a single subject."
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* Action */ },
modifier = Modifier.align(Alignment.End)
) {
Text("Action")
}
}
}
}
Cards group related content and actions, providing visual separation from surrounding elements.
Surface
provides a themed background with elevation:
@Composable
fun SurfaceExample() {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = 4.dp,
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colors.surface,
border = BorderStroke(1.dp, Color.LightGray)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Surface Example",
fontSize = 18.sp,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Surfaces represent areas of the UI that display related content."
)
}
}
}
Surfaces are fundamental to Material Design in Compose, establishing the physical properties of UI elements.
Navigation widgets facilitate movement between different sections of your application.
TopAppBar
provides a consistent navigation header:
@Composable
fun TopAppBarExample() {
Scaffold(
topBar = {
TopAppBar(
title = { Text("App Title") },
navigationIcon = {
IconButton(onClick = { /* Navigate back */ }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* Search action */ }) {
Icon(Icons.Default.Search, contentDescription = "Search")
}
IconButton(onClick = { /* More options */ }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
}
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Text(
text = "Content goes here",
modifier = Modifier.align(Alignment.Center)
)
}
}
}
TopAppBar provides consistent navigation patterns and access to primary actions.
BottomNavigation
enables switching between main sections of an app:
@Composable
fun BottomNavigationExample() {
var selectedItem by remember { mutableStateOf(0) }
val items = listOf("Home", "Search", "Profile")
val icons = listOf(
Icons.Default.Home,
Icons.Default.Search,
Icons.Default.Person
)
Scaffold(
bottomBar = {
BottomNavigation {
items.forEachIndexed { index, item ->
BottomNavigationItem(
icon = {
Icon(icons[index], contentDescription = item)
},
label = { Text(item) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Text(
text = "Selected: ${items[selectedItem]}",
modifier = Modifier.align(Alignment.Center)
)
}
}
}
BottomNavigation provides access to top-level destinations in your app, following Material Design guidelines.
Dialog widgets present focused content that requires user attention or action.
AlertDialog
requests decisions or provides critical information:
@Composable
fun AlertDialogExample() {
var showDialog by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { showDialog = true }) {
Text("Show Dialog")
}
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Dialog Title") },
text = { Text("This is the dialog content that explains the purpose of the dialog.") },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text("Confirm")
}
},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("Dismiss")
}
}
)
}
}
}
AlertDialog follows Material Design guidelines for modal dialogs, providing a consistent user experience for important decisions.
Bottom sheets can be implemented using the ModalBottomSheetLayout
:
@Composable
fun BottomSheetExample() {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Bottom Sheet Content",
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
scope.launch { sheetState.hide() }
}
) {
Text("Close Sheet")
}
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
Button(
onClick = {
scope.launch { sheetState.show() }
},
modifier = Modifier.align(Alignment.Center)
) {
Text("Show Bottom Sheet")
}
}
}
}
Bottom sheets provide access to supplementary content or actions while keeping users in the context of the current screen.
Modifiers are a powerful system in Jetpack Compose that allow you to customize and enhance widgets. They enable developers to create reusable, custom widgets with consistent behavior.
@Composable
fun CustomCard(
title: String,
content: String,
onActionClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
elevation = 8.dp,
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = title,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = content)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onActionClick,
modifier = Modifier.align(Alignment.End)
) {
Text("Action")
}
}
}
}
@Composable
fun CustomWidgetsExample() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
CustomCard(
title = "First Custom Card",
content = "This is a reusable custom card widget created with Compose",
onActionClick = { /* Action */ }
)
CustomCard(
title = "Second Custom Card",
content = "Custom widgets improve code reusability and maintainability",
onActionClick = { /* Action */ },
modifier = Modifier.background(Color.LightGray.copy(alpha = 0.3f))
)
}
}
Creating custom widgets involves encapsulating common UI patterns and behavior into reusable Composable functions. Accepting a Modifier
parameter allows callers to customize the appearance and behavior of your custom widget.
Animations enhance the user experience by providing visual feedback and smooth transitions.
AnimatedVisibility
animates the appearance and disappearance of content:
@Composable
fun AnimatedVisibilityExample() {
var visible by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = { visible = !visible }) {
Text(if (visible) "Hide" else "Show")
}
Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 4.dp
) {
Text(
text = "This content animates in and out",
modifier = Modifier.padding(16.dp)
)
}
}
}
}
AnimatedVisibility combines with enter/exit animations to create smooth transitions when content appears or disappears.
Crossfade
provides smooth transitions between different content:
@Composable
fun CrossfadeExample() {
var currentPage by remember { mutableStateOf("A") }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Button(onClick = { currentPage = "A" }) {
Text("Page A")
}
Button(onClick = { currentPage = "B" }) {
Text("Page B")
}
}
Spacer(modifier = Modifier.height(16.dp))
Crossfade(targetState = currentPage) { screen ->
when (screen) {
"A" -> Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.Blue.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
Text(
text = "Content for Page A",
color = Color.White,
fontSize = 20.sp
)
}
"B" -> Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(Color.Green.copy(alpha = 0.5f)),
contentAlignment = Alignment.Center
) {
Text(
text = "Content for Page B",
color = Color.Black,
fontSize = 20.sp
)
}
}
}
}
}
Crossfade automatically animates transitions between different content states, creating a polished user experience.