Jetpack Compose Widgets

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.

Understanding Compose Fundamentals

Before diving into specific widgets, it's essential to understand the core principles that make Jetpack Compose different from the traditional View-based system.

Declarative UI Paradigm

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.

Composition and Recomposition

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 Management

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.

Essential Text Widgets

Text is fundamental to almost any user interface. Jetpack Compose provides robust text widgets with extensive customization options.

Text Widget

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.

TextField Widget

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:

  • Labels and placeholders for guiding users
  • Keyboard options to control the virtual keyboard type
  • Visual styles like standard or outlined variants
  • Error states for form validation

Layout Widgets

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

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.

Box

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.

Constraint Layout

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

Interactive widgets handle user input and provide visual feedback, forming the basis of user interaction in your Jetpack Compose applications.

Button Variants

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 actions
  • OutlinedButton for secondary actions
  • TextButton for low-emphasis actions
  • IconButton for icon-only actions
  • FloatingActionButton and ExtendedFloatingActionButton for promoted actions

Selection Widgets

Compose 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.

List Widgets

Lists are essential for displaying collections of items. Jetpack Compose provides efficient list widgets optimized for performance.

LazyColumn and LazyRow

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.

LazyVerticalGrid

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

Container widgets in Jetpack Compose provide structure and visual styling to your content.

Card

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

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

Navigation widgets facilitate movement between different sections of your application.

TopAppBar

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

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 and Modal Widgets

Dialog widgets present focused content that requires user attention or action.

AlertDialog

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.

BottomSheet

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.

Custom Widgets with Modifier

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.

Creating Custom Widgets

@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.

Animation Widgets

Animations enhance the user experience by providing visual feedback and smooth transitions.

Animated Visibility

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.

Animated Content

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.