RadioButtons in Jetpack Compose represent selectable options where only one choice can be active at a time. Unlike traditional Android XML layouts, Compose's declarative approach makes RadioButton implementation more straightforward and maintainable. The RadioButton component in Jetpack Compose is designed to be flexible, customizable, and seamlessly integrated with other Compose components.
A RadioButton in Jetpack Compose is typically part of a group of options where selecting one automatically deselects others. This mutual exclusivity makes RadioButtons perfect for scenarios where users must choose exactly one option from a set of alternatives.
To implement a basic RadioButton in Jetpack Compose, you need to understand its fundamental properties:
@Composable
fun RadioButtonExample() {
val radioOptions = listOf("Option 1", "Option 2", "Option 3")
val (selectedOption, onOptionSelected) = remember { mutableStateOf(radioOptions[0]) }
Column {
radioOptions.forEach { text ->
Row(
Modifier
.fillMaxWidth()
.selectable(
selected = (text == selectedOption),
onClick = { onOptionSelected(text) }
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (text == selectedOption),
onClick = { onOptionSelected(text) }
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
In this example, the RadioButton's selected state is controlled by comparing the current text with the selectedOption state variable. When a RadioButton is clicked, the onOptionSelected lambda updates the state, ensuring only one option remains selected.
Understanding the fundamental properties of a RadioButton in Jetpack Compose is crucial for effective implementation:
Unlike the classic Android View system, Jetpack Compose doesn't have a dedicated RadioGroup component. Instead, you create radio groups by sharing state among multiple RadioButton components:
@Composable
fun RadioGroupExample() {
val genderOptions = listOf("Male", "Female", "Non-binary", "Prefer not to say")
val (selectedOption, onOptionSelected) = remember { mutableStateOf(genderOptions[0]) }
Column(Modifier.padding(8.dp)) {
Text(
text = "Select Gender:",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
genderOptions.forEach { gender ->
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.selectable(
selected = (gender == selectedOption),
onClick = { onOptionSelected(gender) }
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (gender == selectedOption),
onClick = { onOptionSelected(gender) }
)
Text(
text = gender,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
This approach uses a shared state variable (selectedOption
) and a callback function (onOptionSelected
) to enforce mutual exclusivity among RadioButtons.
Jetpack Compose offers extensive customization options for RadioButtons to match your application's design language:
@Composable
fun CustomizedRadioButton() {
val options = listOf("Low Priority", "Medium Priority", "High Priority")
val (selectedOption, onOptionSelected) = remember { mutableStateOf(options[0]) }
Column(Modifier.padding(16.dp)) {
options.forEach { option ->
Row(
Modifier
.fillMaxWidth()
.height(48.dp)
.selectable(
selected = (option == selectedOption),
onClick = { onOptionSelected(option) }
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Custom colors for RadioButton
RadioButton(
selected = (option == selectedOption),
onClick = { onOptionSelected(option) },
colors = RadioButtonDefaults.colors(
selectedColor = when(option) {
"Low Priority" -> Color.Green
"Medium Priority" -> Color.Yellow
"High Priority" -> Color.Red
else -> MaterialTheme.colorScheme.primary
},
unselectedColor = Color.Gray,
disabledSelectedColor = Color.LightGray,
disabledUnselectedColor = Color.LightGray
)
)
Text(
text = option,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
This example demonstrates how to customize RadioButton colors based on their content, enhancing visual feedback and user experience.
Effective state management is crucial when working with RadioButtons in Jetpack Compose:
@Composable
fun RadioButtonWithState() {
val transportOptions = listOf("Car", "Bus", "Train", "Airplane", "Ship")
var selectedOption by remember { mutableStateOf("") }
var isSubmitted by remember { mutableStateOf(false) }
Column(Modifier.padding(16.dp)) {
Text(
text = "Select Your Preferred Transport:",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
transportOptions.forEach { option ->
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.selectable(
selected = (option == selectedOption),
onClick = {
selectedOption = option
isSubmitted = false
}
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (option == selectedOption),
onClick = {
selectedOption = option
isSubmitted = false
}
)
Text(
text = option,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { isSubmitted = true },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("Submit")
}
if (isSubmitted) {
Text(
text = "You selected: $selectedOption",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 16.dp)
)
}
}
}
This example demonstrates how to track selection state and respond to user actions with RadioButtons.
RadioButtons often form part of larger form interfaces. Here's how to integrate them effectively:
@Composable
fun RadioButtonInForm() {
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
val subscriptionOptions = listOf("Free", "Monthly", "Annual")
var selectedSubscription by remember { mutableStateOf(subscriptionOptions[0]) }
var isFormSubmitted by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
text = "Subscription Form",
style = MaterialTheme.typography.headlineSmall
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") },
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
modifier = Modifier.fillMaxWidth()
)
Text(
text = "Select Subscription Plan:",
style = MaterialTheme.typography.titleMedium
)
subscriptionOptions.forEach { option ->
Row(
Modifier
.fillMaxWidth()
.height(48.dp)
.selectable(
selected = (option == selectedSubscription),
onClick = { selectedSubscription = option }
)
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (option == selectedSubscription),
onClick = { selectedSubscription = option }
)
Text(
text = option,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
Button(
onClick = { isFormSubmitted = true },
modifier = Modifier.align(Alignment.End)
) {
Text("Submit")
}
if (isFormSubmitted) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(Modifier.padding(16.dp)) {
Text("Form Submitted")
Text("Name: $name")
Text("Email: $email")
Text("Subscription: $selectedSubscription")
}
}
}
}
}
This example shows how RadioButtons can be integrated with text fields and other form elements to create a cohesive user experience.
Accessibility is a crucial aspect of modern Android development. Here's how to make your RadioButtons accessible:
@Composable
fun AccessibleRadioButton() {
val notificationOptions = listOf("All Notifications", "Important Only", "None")
val (selectedOption, onOptionSelected) = remember { mutableStateOf(notificationOptions[0]) }
Column(Modifier.padding(16.dp)) {
Text(
text = "Notification Preferences",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
notificationOptions.forEach { option ->
Row(
Modifier
.fillMaxWidth()
.height(56.dp)
.selectable(
selected = (option == selectedOption),
onClick = { onOptionSelected(option) },
role = Role.RadioButton
)
.semantics {
contentDescription = "Select $option notification preference"
}
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (option == selectedOption),
onClick = null // Handled by the parent's selectable modifier
)
Text(
text = option,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
}
This implementation enhances accessibility by:
Role.RadioButton
semantic rolesemantics
modifier