Jetpack Compose Custom Radio Button UI with Source Code

jetpack compose custom radio button

This is a custom radio button UI design made with Android Jetpack Compose. It displays a list of dogs along with their image, name, and pricing. You can download the source code below.

The UI displays a list of dogs. Each item in the radio group consists of the image, name, age, and cost. I have added an icon that looks like a check box to highlight the selected item. The button colors are set to the primary color of the project.

The Jetpack Compose custom radio button UI consists of a top app bar and a group of items. When you select an item from the group, the others will be deselected automatically. You can only choose one item at a time.

I have disabled the ripple effect on the group to make the selection easier. The single-item UI consists of multiple Row and Column layouts. There are 3 items on the list. If your data is large, enable vertical scrolling on the Column layout.

The Box is the parent layout of each item. The dog’s image and details like name and price are placed in a Row layout. I’m using the Roboto font in the project. The check box icon looks beautiful on the right side of the Box.

Before adding the code to your project, you should download the latest version of Android Studio. The Jetpack Compose doesn’t work in the older versions.

Final Output:

If the video isn’t working, watch it on the YouTube.

Helpful Links to Understand the Code:

Resources Used in the Project:

Here are the Gradle files used in the project.

You can download the dog images from Pixabay. Crop them into a square shape before adding to the project.

Download the Roboto Font from Google Fonts.

Jetpack Compose Custom Radio Buttons Source Code:

MainActivity.kt:

import android.content.Context
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material.ripple.RippleAlpha
import androidx.compose.material.ripple.RippleTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

/*
You can use the following code for commercial purposes with some restrictions.
Read the full license here: https://semicolonspace.com/semicolonspace-license/
For more designs with source code, visit:
https://semicolonspace.com/jetpack-compose-samples/
 */

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BlogPostsTheme(darkTheme = false) {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {

                    val contextForToast = LocalContext.current.applicationContext

                    Column(
                        modifier = Modifier.fillMaxSize()
                    ) {

                        MyTopAppBar(contextForToast = contextForToast)

                        Spacer(modifier = Modifier.height(height = 8.dp))

                        Text(
                            modifier = Modifier.padding(start = 16.dp),
                            text = "Select a Dog",
                            fontSize = 24.sp,
                            fontFamily = FontFamily(Font(R.font.roboto_bold, FontWeight.Bold))
                        )

                        Spacer(modifier = Modifier.height(height = 8.dp))

                        CustomRadioButtons()
                    }
                }
            }
        }
    }
}

@Composable
private fun MyTopAppBar(contextForToast: Context) {

    TopAppBar(
        title = { Text(text = "Dogs") },
        backgroundColor = Color.White,
        navigationIcon = {
            IconButton(
                onClick = {
                    Toast.makeText(
                        contextForToast,
                        "Nav Icon Click",
                        Toast.LENGTH_SHORT
                    ).show()
                }
            ) {
                Icon(
                    imageVector = Icons.Outlined.ArrowBack,
                    contentDescription = "Go Back"
                )
            }
        }
    )
}

@Composable
private fun CustomRadioButtons() {

    val dogsList = returnDogsList()

    var selectedItem by remember {
        mutableStateOf(dogsList[0].name)
    }

    Column(
        modifier = Modifier
            //.verticalScroll(state = rememberScrollState())
            .selectableGroup()
    ) {

        CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {

            dogsList.forEach { dogDetails ->
                Row(
                    modifier = Modifier
                        .fillMaxWidth()
                        .selectable(
                            selected = (selectedItem == dogDetails.name),
                            onClick = { selectedItem = dogDetails.name },
                            role = Role.RadioButton
                        )
                        .padding(vertical = 8.dp)
                ) {
                    RadioButtonStyle(selectedItem = selectedItem, dogDetails = dogDetails)
                }
            }
        }
    }
}

@Composable
private fun RadioButtonStyle(selectedItem: String, dogDetails: DogsData) {
    Box(
        modifier = Modifier
            .padding(horizontal = 16.dp, vertical = 4.dp)
            .fillMaxWidth()
            .border(
                width = 1.dp,
                color =
                if (selectedItem == dogDetails.name)
                    MaterialTheme.colors.primary
                else
                    Color.LightGray,
                shape = RoundedCornerShape(percent = 10)
            )
            .padding(horizontal = 20.dp, vertical = 16.dp)
    ) {
        Row(modifier = Modifier.fillMaxWidth()) {

            // dog's image
            Image(
                modifier = Modifier
                    .size(size = 94.dp)
                    .clip(shape = CircleShape),
                painter = painterResource(id = dogDetails.image),
                contentDescription = "Dog Image"
            )

            // dog's details
            Column(
                modifier = Modifier
                    .padding(start = 16.dp, top = 6.dp, bottom = 6.dp)
                    .fillMaxWidth()
            ) {

                Row(
                    modifier = Modifier.fillMaxWidth(),
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {

                    // dog's name
                    Text(
                        text = dogDetails.name,
                        fontSize = 22.sp,
                        fontFamily = FontFamily(
                            Font(
                                R.font.roboto_bold,
                                weight = FontWeight.Bold
                            )
                        )
                    )

                    // check icon
                    Icon(
                        imageVector =
                        if (selectedItem == dogDetails.name)
                            Icons.Outlined.CheckCircle
                        else
                            Icons.Outlined.RadioButtonUnchecked,
                        contentDescription = null,
                        tint =
                        if (selectedItem == dogDetails.name)
                            MaterialTheme.colors.primary
                        else
                            Color.Gray
                    )
                }

                // dog's age
                Text(
                    modifier = Modifier.padding(top = 4.dp),
                    text = "${dogDetails.age}-old",
                    fontSize = 18.sp,
                    fontFamily = FontFamily(
                        Font(
                            R.font.roboto_regular,
                            weight = FontWeight.Normal
                        )
                    )
                )

                // dog's price
                Text(
                    modifier = Modifier.padding(top = 6.dp),
                    text = "$${dogDetails.price}",
                    fontSize = 22.sp,
                    fontFamily = FontFamily(Font(R.font.roboto_medium, weight = FontWeight.Medium)),
                    color = Color.DarkGray
                )
            }
        }
    }
}

private fun returnDogsList(): ArrayList<DogsData> {
    val dogsList = arrayListOf<DogsData>()

    dogsList.add(
        DogsData(
            image = R.drawable.dog3,
            name = "Prince",
            age = "3-year",
            price = "250"
        )
    )

    dogsList.add(
        DogsData(
            image = R.drawable.dog2,
            name = "Lucky",
            age = "2-year",
            price = "500"
        )
    )

    dogsList.add(
        DogsData(
            image = R.drawable.dog1,
            name = "Frankie",
            age = "5-year",
            price = "300"
        )
    )

    return dogsList
}

object NoRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = Color.Unspecified

    @Composable
    override fun rippleAlpha(): RippleAlpha = RippleAlpha(0.0f, 0.0f, 0.0f, 0.0f)
}

data class DogsData(val image: Int, val name: String, val age: String, val price: String)

Related:

Leave a Comment