Jetpack Compose RadioButton

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.

Basic Implementation of RadioButton

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.

Key Properties of RadioButton

Understanding the fundamental properties of a RadioButton in Jetpack Compose is crucial for effective implementation:

  1. selected: A Boolean parameter that determines whether the RadioButton is currently selected.
  2. onClick: A lambda function that's invoked when the RadioButton is clicked.
  3. modifier: Optional Modifier for applying styling, layout, and behavior to the RadioButton.
  4. enabled: A Boolean parameter that controls whether the RadioButton can be interacted with.
  5. colors: RadioButtonColors to customize the appearance of the RadioButton in different states.

Creating a RadioGroup in Jetpack Compose

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.

Customizing RadioButton Appearance

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.

Handling RadioButton States and Events

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.

Integrating RadioButtons with Forms

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 Considerations for RadioButton

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:

  • Using the Role.RadioButton semantic role
  • Adding content descriptions with the semantics modifier
  • Ensuring the entire row is tappable (not just the RadioButton itself)