Android Fundamentals: RGB Color Mixer

Introduction
Hi again, dear readers!
In the last post we had a brief introduction to Jetpack Compose, and before that a brief introduction to Android app components. In today's article, we'll build a color mixer app as our first project to have a brief introduction to Android app development.
In this article I'll document my walkthrough and my decisions, I encourage you, dear readers, to explore different approaches for the project and feedback, all suggestions are welcome.
Project Description
We'll build a color mixer, we'll have three text fields that will accept hexadecimal values for red, green and blue channels respectively. It'll also contain a preview of the resulting color and a label of the color's hex code. This should look like the image below:

Of course, the app must validate inputs and shouldn't crash. To run the app, I'll use a real device because my potato would burn if I try to run the emulator, but here you'll find a guide to configure the emulator in your machine.
Setting Up Your First Project
Download Android Studio, run it and create a new project. From the options, keep "Empty Activity" selected and press Next.
Rename "My Application" to "RGB Color Mixer", you can change other details, but be careful when changing Minimum SDK, the older, the better for compatibility with older devices. I'm using Minimum SDK 24 to support more devices. Then press Finish, let Gradle finish the sync and you're all done.
Along the projects, we'll explore the project structure when needed, we can ignore it for now.
Source Code
The default source code has two parts, the Activity declaration and the composable functions.
The default main activity is literally named as MainActivity. All Activities must be a subclass of the Activity class, as we can observe from the source code below, we have ComponentActivity, which is also a subclass of the Activity class. If the syntax looks confusing, check for trailing lambdas.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
RGBColorMixerTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
The function setContent is responsible for setting up the layout. Inside of it there are other function calls, let's focus on the Greeting function, that is declared as below by default.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
RGBColorMixerTheme {
Greeting("Android")
}
}
We can notice the annotation @Composable, this indicates that it is a composable, There is also a second annotation, the @Preview.
Development Process
Auxiliary Functions
Before paying attention to the UI, I first created functions that would help me handle user input, validate it, properly convert it to Color class, and use the color to update the preview.
The private val HEX_DIGITS = "0123456789ABCDEF" variable was declared to help to validate hex values and convert their string representation to integer.
The function isValidChannel validates the user input by checking the size and the characters it contains.
private fun isValidChannel(channel: String): Boolean{
var isValid = true
if (channel.length !in 1..2)
return false
channel.forEach { isValid = isValid && HEX_DIGITS.contains(it.uppercase()) }
return isValid
}
To convert hexadecimal strings to Int, I declared an extension function for String class. This is just an adaptation of the famous atoi function (C programmers will recognize it), which converts strings to integers.
private fun String.toColorChannel(): Int{
var value = 0
if (!isValidChannel(this))
return value
this.forEach {
value = value.shl(4)
value += HEX_DIGITS.indexOf(it.uppercase())
}
return value
}
Finally, to get a Color object, I create the getColor function. This used bitwise operations to create a valid Int representation of a color, that follows the 0xAARRGGBB hexadecimal pattern. Here you'll find the API reference for more details.
private fun getColor(red: String, green: String, blue: String): Color {
return Color(0xff.shl(8)
.or(red.toColorChannel())
.shl(8)
.or(green.toColorChannel())
.shl(8)
or(blue.toColorChannel())
)
}
Resources
Resources are files and static content that our code uses, such as images, strings, etc. Always externalize resources, so that we can maintain them separately. Also provide alternative resources, at runtime Android uses the appropriate resource based on current configuration — images for different screen sizes, for example.
Resources are located at app/src/main/res/ folder, there are different folders for different resources in it. For this project I focused on the values/ folder, strings.xml to be more accurate.
This file is dedicated to string resources, instead of using string literals inside your code, just set a string resource and use stringResource to get it inside your code. For example, let's say we have the following string resource:
<resources>
<string name="app_name">RGB Color Mixer</string>
...
</resources>
To access it, we can call stringResource(R.string.app_name) to access the string resource. Every string resource has an Int ID generated inside of project's R class. In this example R.string.app_name is the resource ID.
Declaring The UI
I first removed 'Greeting' and 'GreetingPreview' declarations and calls, then created a composable to get the user input as follows:
@Composable
fun ColorTextField(
modifier: Modifier = Modifier,
color:String = "",
isValidColor: Boolean = true,
label: Int,
placeholder: Int,
onValueChange: (String) -> Unit = {},
){
TextField(
modifier = modifier.padding(4.dp),
value = color,
onValueChange = onValueChange,
isError = !isValidColor,
supportingText = {
if (!isValidColor)
Text(
text = stringResource(R.string.invalid_color),
color = MaterialTheme.colorScheme.error
)
},
trailingIcon = {
if (!isValidColor)
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = stringResource(R.string.error_icon),
tint = MaterialTheme.colorScheme.error
)
},
singleLine = true,
label = {
Text(stringResource(label))
},
placeholder = {
Text(stringResource(placeholder))
}
)
}
The TextField composable is brought by Jetpack Compose, other default composables used in the project are Button and Text, there are many others, but we'll explore them along future projects. To avoid repetition, I declared a customized TextField to reuse common details through ColorTextField composable.
It's worth noting that value and onValueChange must be set in TextField. value is the current TextField content while onValueChange is a lambda function that will be executed every time value changes. As Jetpack Compose uses a declarative approach and composables can only be updated through recomposition, remember API and MutableState<T> objects should be used to trigger it.
To preview a color, the Surface composable was used, its height was defined to 1/3 of the screen height.
Modifiers allow us to augment or decorate composables. It's a best practice to have all our composable functions to accept a modifier parameter and pass that parameter to the first child that emits UI.
@Composable
private fun ColorPreview(
modifier: Modifier = Modifier,
color: Color,
label: @Composable () -> Unit = {}
){
Column(
modifier = modifier.fillMaxSize().padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
){
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
Surface(
modifier = Modifier.fillMaxWidth().height(screenHeight / 3),
color = color
){}
label()
}
}
If you noticed, the code has two interesting composables: Column and Row. Including Box composable, they form the layout composables, making easier to design our UI.
Column organizes UI elements vertically, Row horizontally, and Box stacks elements one above other.
To put things together, I declared the ColorMixerUI composable as follows:
@Preview
@Composable
fun ColorMixerUI(
modifier: Modifier = Modifier,
){
var red by rememberSaveable { mutableStateOf("") }
var green by rememberSaveable { mutableStateOf("") }
var blue by rememberSaveable { mutableStateOf("") }
var isValidRed by rememberSaveable { mutableStateOf(true) }
var isValidGreen by rememberSaveable { mutableStateOf(true) }
var isValidBlue by rememberSaveable { mutableStateOf(true) }
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Row(
modifier = Modifier.fillMaxWidth().padding(4.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
ColorTextField(
modifier = Modifier.weight(1f),
color = red,
isValidColor = isValidRed,
label = R.string.red,
placeholder = R.string.input_red
){
if (it.length < 3 ) {
red = it.uppercase()
isValidRed = isValidChannel(it)
}
}
ColorTextField(
modifier = Modifier.weight(1f),
color = green,
isValidColor = isValidGreen,
label = R.string.green,
placeholder = R.string.input_green
){
if (it.length < 3 ) {
green = it.uppercase()
isValidGreen = isValidChannel(it)
}
}
ColorTextField(
modifier = Modifier.weight(1f),
color = blue,
isValidColor = isValidBlue,
label = R.string.blue,
placeholder = R.string.input_blue
){
if (it.length < 3 ) {
blue = it.uppercase()
isValidBlue = isValidChannel(it)
}
}
}
ColorPreview(
modifier = Modifier,
color = getColor(red, green, blue),
){
Text(
modifier = Modifier
.padding(8.dp),
text = "#${getColorString(red, green, blue).uppercase()}",
fontSize = 24.sp,
textAlign = TextAlign.Center,
)
}
Button(
modifier = Modifier.padding(vertical = 8.dp),
onClick = {
red = ""
green = ""
blue = ""
}
) {
Text(stringResource(R.string.reset))
}
}
}
Handling State
Ignoring my questionable UI design decisions, let's focus on this part of ColorMixerUI:
var red by rememberSaveable { mutableStateOf("") }
var green by rememberSaveable { mutableStateOf("") }
var blue by rememberSaveable { mutableStateOf("") }
var isValidRed by rememberSaveable { mutableStateOf(true) }
var isValidGreen by rememberSaveable { mutableStateOf(true) }
var isValidBlue by rememberSaveable { mutableStateOf(true) }
That is the use of the remember API and MutableState<T> to trigger recomposition and update the app's UI state. rememberSaveable is the variant of remember that survives configuration changes (eg: screen rotation).
Final Words
This project is a brief introduction to Android development, you now have an idea on how to put things together and build basic UIs. Check the references for more details, the project repository is also there.
See you in the next post, happy coding!
References
Jetpack Compose UI App Development Toolkit
ComponentActivity API Reference
EOF



