Choices of Choices: Exploring User Selection Components in Jetpack Compose with Material 3

Kerry Bisset
16 min readJun 24, 2024

--

Imagine you are on a development team without a dedicated UI or UX designer. How do you ensure your app’s intuitive and visually appealing user interface? This is a common scenario, especially in smaller teams or startups, where developers wear multiple hats. This is where Material 3, combined with Jetpack Compose, becomes a game-changer.

Small Introduction to Material 3

Material 3, or Material You, is the latest iteration of Google’s design system. It emphasizes personal aesthetics and adaptability, making creating unique and user-centric interfaces easier. Material 3 includes detailed guidelines for each component, offering insights into their best use cases, design considerations, and implementation tips. These guidelines cover accessibility, responsiveness, and theming, ensuring that each component can be used effectively in various contexts. This article distills this information into a concise and practical list, helping you quickly understand and implement the right components for capturing user choices.

Jetpack Compose

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs. Integrating Material 3 with Jetpack Compose means you get the best of both worlds: a robust design system and a flexible, declarative UI framework.

Benefits for Teams Without UI/UX Designers

  1. Consistency: Material 3 provides pre-designed components that follow best practices for usability and aesthetics. This ensures your app maintains a professional and consistent look and feel, even without a UI/UX designer.
  2. Efficiency: With Jetpack Compose, you can quickly implement these components, saving time and reducing the complexity of your codebase.
  3. Customization: While Material 3 offers default styles, it also allows for extensive customization to match your brand’s identity and your app's specific needs.

By leveraging Material 3 and Jetpack Compose, even development teams without dedicated designers can create beautiful, user-friendly interfaces.

Overview of Material 3 in Jetpack Compose

Key Features of Material 3

  1. Personalization: Material 3 allows for extensive customization, enabling users to tailor the app’s look and feel based on their preferences. This includes dynamic color schemes that can be adjusted according to the user’s wallpaper and settings.
  2. Adaptability: Components in Material 3 are designed to be highly adaptable, ensuring they look great on various devices and screen sizes. This provides a consistent user experience across different platforms, especially if you focus on KMP.
  3. Accessibility: Material 3 emphasizes accessibility, making it easier for developers to create inclusive interfaces that cater to all users, including those with disabilities.

Integration with Jetpack Compose

Jetpack Compose is Android’s toolkit for declaratively building native UIs. It complements Material 3 by providing a powerful framework that simplifies UI development. With Compose, you write less code than traditional XML-based layouts, making the code more readable and maintainable.

Benefits of Using Jetpack Compose with Material 3

  1. Simplified Development: Compose’s declarative nature allows you to describe your UI in Kotlin, making the codebase more concise and easier to manage. This speeds up development and reduces the likelihood of bugs.
  2. Live Previews and Hot Reload: Compose offers live previews and hot reload features, significantly enhancing the development experience. You can see changes in real-time, making it easier to iterate on designs and quickly implement feedback.
  3. Interoperability: Compose is designed to work with existing Android views and XML layouts, allowing you to adopt it incrementally in your projects.

Choosing the Right UI Components

In this article, we will evaluate the user selection components provided by Material 3 and Jetpack Compose, focusing on their best use cases and implementation strategies. We will cover checkboxes, radio buttons, filter chips, segmented buttons, drop-down lists, switches, sliders, and toggle buttons. Each component has distinct advantages and scenarios, ensuring you can select the most appropriate one for your application’s needs. You can create a polished and user-friendly interface without specialized design skills by understanding and leveraging these components.

Checkboxes

Usage and Benefits

Checkboxes are a fundamental UI component for binary choices or selections. They are ideal when users need to select multiple options from a list independently. Checkboxes provide a clear and familiar way for users to interact with your app.

Best Use Cases:

  1. Settings and Preferences: Use checkboxes to toggle settings and preferences where multiple selections are allowed.
  2. Task Lists: Ideal for to-do lists where users need to mark tasks as complete or incomplete.
  3. Forms: Useful when in forms where users must agree to multiple terms or select multiple options.

Implementation Example

@Preview(showBackground = true)
@Composable
fun CheckboxExamplePreview() {
CheckboxExample()
}

@Composable
fun CheckboxExample() {
val checkedState = remember { mutableStateOf(true) }

Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
text = if (checkedState.value) "Checked" else "Unchecked",
style = MaterialTheme.typography.bodyLarge,
)
Checkbox(
checked = checkedState.value,
onCheckedChange = { checkedState.value = it },
)
}
}

Customization

These components provide extensive customization options for checkboxes, allowing you to adapt them to your app’s design language. You can customize the appearance by modifying color, shape, and size.

@Composable
fun CustomCheckboxExample(startingState: Boolean = false) {
val checkedState = remember { mutableStateOf(startingState) }

Checkbox(
checked = checkedState.value,
onCheckedChange = { checkedState.value = it },
colors = CheckboxDefaults.colors(
checkedColor = Color.Red,
uncheckedColor = Color.Gray,
checkmarkColor = Color.White
)
)
}

@Preview(showBackground = true)
@Composable
fun CustomCheckboxExamplePreview() {
Column {
CustomCheckboxExample()
CustomCheckboxExample(true)
}
}

In this example, the CheckboxDefaults.colors The function customizes the checkbox's colors. You can similarly customize other aspects to fit your app's theme.

Considerations for Larger Sets of Options

While checkboxes are versatile, they may become cumbersome when dealing with many options, especially if the list requires scrolling. In such cases, consider using selectable cards, which provide a larger touch target and can enhance the user experience.

Selectable Cards for Large Option Sets

Selectable cards can be used in place of checkboxes when you have a long list of options. They are more visually engaging and easier for users to interact with, especially on touch devices.

@Composable
fun SelectableCardExample(startingState: Boolean = false) {
val selected = remember { mutableStateOf(startingState) }

Card(
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(
containerColor = if (selected.value) {
MaterialTheme.colorScheme.tertiaryContainer
} else {
MaterialTheme.colorScheme.primaryContainer
}
),
modifier = Modifier
.padding(8.dp)
.clickable { selected.value = !selected.value },
elevation = CardDefaults.cardElevation(4.dp)
) {
Text(
text = if (selected.value) "Selected" else "Not Selected",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(16.dp)
)
}
}

@Preview(showBackground = true)
@Composable
fun SelectableCardExamplePreview() {
MaterialTheme {
Row {
SelectableCardExample()
SelectableCardExample(true)
}
}
}

In this example, a Card The component creates a selectable card. When selected, the card changes its background color, providing a clear visual cue to the user.

Radio Buttons

Usage and Benefits

Radio buttons are a classic UI component used to make a single selection from a list of mutually exclusive options. They are easy to understand and provide a clear visual indication of the selected option. Despite being somewhat dated, radio buttons remain effective for simple, straightforward choices.

Best Use Cases

  1. Forms: This is ideal for form fields where a user needs to select one option from a list, such as a payment method or a shipping option.
  2. Settings: This is useful in settings screens for choosing a single option from a set of configurations, like selecting a theme (light or dark mode).
  3. Surveys and Questionnaires: These are perfect for survey questions where users must choose one answer from a list of possible responses.
@Composable
fun RadioButtonExample() {
val selectedOption = remember { mutableStateOf("Option 1") }

Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Choose an option:",
style = MaterialTheme.typography.bodyLarge
)
listOf("Option 1", "Option 2", "Option 3").forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
RadioButton(
selected = selectedOption.value == option,
onClick = { selectedOption.value = option }
)
Text(
text = option,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}

@Preview(showBackground = true)
@Composable
fun RadioButtonExamplePreview() {
RadioButtonExample()
}

In this example, a RadioButton component is used to create a set of radio buttons. The state of the selected option is managed using Compose's remember and mutableStateOf functions. Each radio button is part of a list, making it easy to scale the number of options.

Customization

@Composable
fun CustomRadioButtonExample() {
val selectedOption = remember { mutableStateOf("Custom Option 1") }

Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Choose a custom option:",
style = MaterialTheme.typography.bodyLarge
)
listOf("Custom Option 1", "Custom Option 2", "Custom Option 3").forEach { option ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 4.dp)
) {
RadioButton(
selected = selectedOption.value == option,
onClick = { selectedOption.value = option },
colors = RadioButtonDefaults.colors(
selectedColor = Color.Green,
unselectedColor = Color.Red
)
)
Text(
text = option,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}

@Preview(showBackground = true)
@Composable
fun CustomRadioButtonExamplePreview() {
CustomRadioButtonExample()
}

In this example, the RadioButtonDefaults.colors function is used to customize the colors of the radio buttons. You can similarly customize other aspects to fit your app's theme.

Considerations for Larger Sets of Options

While radio buttons are useful, they can become unwieldy if too many options exist, especially if the list requires scrolling. Consider using a drop-down list, which can present many options compactly.

Drop-Down Lists for Large Option Sets

When you have a long list of options, Drop-down lists can be used instead of radio buttons. They save screen space and are more manageable for users.

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DropdownExample(initialState: Boolean) {
var expanded by remember { mutableStateOf(initialState) }
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
var selectedOption by remember { mutableStateOf(options[0]) }

ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
TextField(
value = selectedOption,
onValueChange = {},
readOnly = true,
label = { Text("Choose an option") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
modifier = Modifier.padding(16.dp)
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption) },
onClick = {
selectedOption = selectionOption
expanded = false
}
)
}
}
}
}

@Preview(showBackground = true)
@Composable
fun DropdownExamplePreview() {
Column {
DropdownExample(false)
}
}

In this example, an ExposedDropdownMenuBox and ExposedDropdownMenu are used to create a drop-down list. This provides a compact and user-friendly way to present many options.

Radio buttons are effective for simple, single-selection scenarios, but drop-down lists offer a more efficient solution for larger sets of options.

Filter Chips

Usage and Benefits

Filter chips are modern UI components that apply or remove filters from a list or dataset. They are an excellent alternative to checkboxes, especially when dealing with a limited number of options (typically less than six). Filter chips provide a larger interaction area, making them more user-friendly, and they can include icons to enhance visual communication.

Best Use Cases

  1. Filtering Lists: These are ideal for applying filters to a list of items, such as filtering products in an e-commerce app by category or price range.
  2. Search Filters: Useful in search interfaces where users can refine search results by selecting multiple filter criteria.
  3. Tag Selection: This tool is perfect for selecting tags or categories, such as in a content management system, where articles can be tagged with relevant topics.

Implementation Example

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterChipsExample(initialSelections: List<String>) {
val filters = listOf("Filter 1", "Filter 2", "Filter 3")
val selectedFilters =
remember { mutableStateListOf<String>().also { it.addAll(initialSelections) } }

Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Select filters:",
style = MaterialTheme.typography.bodyLarge
)
Row(modifier = Modifier.padding(top = 8.dp)) {
filters.forEach { filter ->
FilterChip(
selected = selectedFilters.contains(filter),
onClick = {
if (selectedFilters.contains(filter)) {
selectedFilters.remove(filter)
} else {
selectedFilters.add(filter)
}
},
label = { Text(filter) },
modifier = Modifier.padding(end = 8.dp)
)
}
}
}
}

@Preview(showBackground = true)
@Composable
fun FilterChipsExamplePreview() {
Column {
FilterChipsExample(emptyList())
FilterChipsExample(initialSelections = listOf("Filter 1"))
}
}

In this example, a FilterChip component is used to create a set of filter chips. The state of the selected filters is managed using Compose's remember and mutableStateListOf functions. Each chip toggles its selected state when clicked.

Customization

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomFilterChipsExample(initialSelections: List<String>) {
val filters = listOf("Filter 1", "Filter 2", "Filter 3")
val selectedFilters =
remember { mutableStateListOf<String>().also { it.addAll(initialSelections) } }

Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Select custom filters:",
style = MaterialTheme.typography.bodyLarge
)
Row(modifier = Modifier.padding(top = 8.dp)) {
filters.forEach { filter ->
FilterChip(
selected = selectedFilters.contains(filter),
onClick = {
if (selectedFilters.contains(filter)) {
selectedFilters.remove(filter)
} else {
selectedFilters.add(filter)
}
},
label = { Text(filter) },
leadingIcon = { if (selectedFilters.contains(filter)) Icon(Icons.Filled.Check, contentDescription = null) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary,
selectedLabelColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.surface,
labelColor = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.padding(end = 8.dp)
)
}
}
}
}

@Preview(showBackground = true)
@Composable
fun CustomFilterChipsExamplePreview() {
Column {
CustomFilterChipsExample(emptyList())
CustomFilterChipsExample(initialSelections = listOf("Filter 3"))
}
}

Benefits Over Checkboxes

  1. Larger Interaction Area: Filter chips offer a bigger touch target, improving usability on touch devices.
  2. Visual Enhancements: The ability to include icons and customize colors makes filter chips more visually appealing and informative.
  3. Modern Look: Filter chips provide a contemporary UI element that enhances your app's look and feel.

By leveraging filter chips in Jetpack Compose with Material 3, you can create a user-friendly and visually appealing interface for applying filters. This component is particularly useful for scenarios with limited options, ensuring a smooth and intuitive user experience.

Segmented Buttons

Usage and Benefits

Segmented buttons are a stylish and modern alternative to radio buttons. They allow users to make a single selection from a small set of mutually exclusive options. Segmented buttons provide a clear and compact way to present options horizontally, making them particularly useful for settings or filter selections that must fit within a single row.

Best Use Cases

  1. Mode Selection: This is ideal for switching between modes, such as view modes (list, grid) or themes (light, dark).
  2. Category Selection: Useful in scenarios where users need to choose a category, like selecting a news category (technology, sports, entertainment).
  3. Filters: Effective for filters that apply to a dataset, such as sorting options (newest, oldest, most popular).

Considerations for Usage

Segmented buttons must fit within a single row, so testing their appearance on small screens is important to ensure usability. If the options do not fit comfortably, consider using a different UI element or revisiting the design.

Implementation Example

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SegmentedButtonExample() {
val options = listOf("Option 1", "Option 2", "Option 3")
var selectedOption by remember { mutableStateOf(options[0]) }

Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Choose an option:",
style = MaterialTheme.typography.bodyLarge
)
SingleChoiceSegmentedButtonRow {
options.forEachIndexed { index, option ->
SegmentedButton(
selected = selectedOption == option,
onClick = { selectedOption = option },
shape = SegmentedButtonDefaults.itemShape(
index = index,
count = options.size
)
) {
Text(text = option)
}
}

}
}
}

@Preview(showBackground = true)
@Composable
fun SegmentedButtonExamplePreview() {
SegmentedButtonExample()
}

Considerations for Small Screens

Segmented buttons must fit within a single row, making testing their appearance on small screens important (see below). Ensure the text size and padding are adjusted to make all options visible and easily tappable. Consider using a drop-down list or another UI component if the buttons do not fit.

Benefits Over Radio Buttons

  1. Modern Look: Segmented buttons provide a contemporary and sleek appearance, enhancing your app's overall look and feel.
  2. Compact Layout: They present options in a horizontal layout, which can save vertical space and make the UI cleaner.
  3. Interactive Feedback: Segmented buttons can provide immediate visual feedback on selection, improving user experience.

Drop-Down Lists

Usage and Benefits

While often considered a standard and unexciting UI component, drop-down lists are versatile and suitable for many use cases. They allow users to choose one option from a long list without taking up much screen space, making them ideal for scenarios where screen real estate is limited or the number of options is large.

Best Use Cases

  1. Form Fields: Ideal for selecting options in forms, such as country, state, or city.
  2. Settings: This is useful in settings screens where users must select from a list of options, like a language or a time zone.
  3. Filters: Effective for applying filters to a dataset, such as choosing a category or sorting order.
  4. Menus: Perfect for navigation menus where space is limited, allowing users to select a section or page.

Implementation Example

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DropdownExample(initialState: Boolean) {
var expanded by remember { mutableStateOf(initialState) }
val options = listOf("Option 1", "Option 2", "Option 3", "Option 4", "Option 5")
var selectedOption by remember { mutableStateOf(options[0]) }

ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
TextField(
value = selectedOption,
onValueChange = {},
readOnly = true,
label = { Text("Choose an option") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
modifier = Modifier.padding(16.dp)
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption) },
onClick = {
selectedOption = selectionOption
expanded = false
}
)
}
}
}
}

@Preview(showBackground = true)
@Composable
fun DropdownExamplePreview() {
Column {
DropdownExample(false)
}
}

Benefits of Drop-Down Lists

  1. Space Efficiency: Drop-down lists are compact and save screen space, making them ideal for mobile devices and small screens.
  2. Scalability: They can handle many options without cluttering the UI.
  3. Usability: Familiar and easy for users to understand and interact with.

Considerations for Usage While drop-down lists are versatile and useful, it’s essential to ensure they are easy to use and accessible. This includes:

  1. Clear Labels: Use descriptive labels for the drop-down list and its options to clarify users' choices.
  2. Accessibility: Ensure the drop-down list is accessible by providing appropriate labels and using accessible colors and font sizes.
  3. Test on Different Devices: Verify the usability of the drop-down list on various devices and screen sizes to ensure a consistent user experience.

Drop-down lists are a staple in UI design for a reason. They provide a clean, efficient way to present many options without overwhelming the user.

Other User Choice Components

In addition to checkboxes, radio buttons, filter chips, segmented buttons, and drop-down lists, several other user selection UI components exist. These components serve various purposes and can appropriately enhance your app’s user experience. Here, we will cover switches, sliders, and toggle buttons, providing basic use cases for each.

Switches

Switches are used for binary choices, typically to turn a setting on or off. They are similar to checkboxes but are often preferred for settings because they provide a more visual and interactive way of toggling states.

Best Use Cases

  1. Settings Toggles: Ideal for enabling or disabling settings such as notifications, dark mode, or sound.
  2. Feature Toggles: Useful for turning features on or off, like enabling experimental features in an app.

Implementation Example

@Composable
fun SwitchExample() {
var isChecked by remember { mutableStateOf(true) }

Switch(
checked = isChecked,
onCheckedChange = { isChecked = it },
modifier = Modifier.padding(16.dp),
)
}

@Preview(showBackground = true)
@Composable
fun SwitchExamplePreview() {
SwitchExample()
}

Sliders

Sliders allow users to select a value from a continuous or discrete range. They are ideal for settings that require a range of values, such as volume control or brightness adjustment.

Best Use Cases

  1. Volume Control: Adjusting the volume level in a media player.
  2. Brightness Adjustment: Changing the screen brightness.
  3. Range Selection: Selecting a value within a specific range, such as setting a budget limit.

Implementation Example

@Composable
fun SliderExample() {
var sliderPosition by remember { mutableStateOf(10f) }

Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..100f,
modifier = Modifier.padding(16.dp)
)
}

@Preview(showBackground = true)
@Composable
fun SliderExamplePreview() {
SliderExample()
}

Toggle Buttons

Toggle buttons are used for binary choices where users can switch between two states. They are often used in toolbars or action bars to enable or disable specific modes or features.

Best Use Cases

  1. Mode Switching: Toggling between edit and view modes in a text editor.
  2. Feature Enable/Disable: Turning a feature on or off, like enabling GPS tracking.

Implementation Example

@Composable
fun ToggleButtonExample() {
var isToggled by remember { mutableStateOf(false) }

Button(
onClick = { isToggled = !isToggled },
colors = ButtonDefaults.buttonColors(
containerColor = if (isToggled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
contentColor = if (isToggled) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.padding(16.dp)
) {
Text(if (isToggled) "On" else "Off")
}
}

@Preview(showBackground = true)
@Composable
fun ToggleButtonExamplePreview() {
ToggleButtonExample()
}

Other Considerations for User Selection UIs

When choosing which user selection UI to use, consider the following:

  1. Context: Ensure the component fits the context in which it is used. For example, sliders are great for ranges, but switches are better for binary states.
  2. Screen Size: Make sure the component looks good and is usable on different screen sizes.
  3. User Experience: Aim for intuitive and straightforward interactions. Avoid overwhelming users with too many options in a small space.

Recap:

Throughout this article, we’ve explored various user selection components available in Jetpack Compose with Material 3, each tailored to specific use cases. From checkboxes and radio buttons to filter chips, segmented buttons, drop-down lists, switches, sliders, and toggle buttons, each component enhances user interaction and experience.

Final Thoughts

Implementing the right user selection components can significantly impact your app's usability and appeal. Material 3’s design principles and Jetpack Compose’s declarative UI approach offer a powerful toolkit for creating intuitive and visually consistent interfaces. Whether you’re dealing with simple binary choices or more complex multi-selection scenarios, there’s a component that fits your needs.

Key Takeaways

  1. Customization: Material 3 allows extensive customization, enabling you to tailor components to your app’s unique style and requirements. Granted, the Material theme covers much of this.
  2. Usability: Choose the right component for the task to ensure a smooth and intuitive user experience. Consider the context, number of options, and screen size when choosing.
  3. Efficiency: Jetpack Compose streamlines UI development, making implementing and managing these components faster and easier.

--

--