A Simple Binary To Decimal Converter App In Jetpack Compose

I’ve been learning Jetpack Compose and Kotlin (and Android for that matter) so I decided to create a simple binary conversion app to demonstrate how easy it is to create (at least basic) UI in Compose.

https://www.exploringbinary.com/wp-content/uploads/Android.ByteValueOfDecimal67.png
Byte to Decimal Converter Demo App (Pixel 4 Emulator)

Overview

My demo app consists of four composable functions:

  • ByteToDecimal()
  • Byte()
  • Bit()
  • Decimal()

ByteToDecimal() is the top-level composable which I call from setContent() in MainActivity.kt. (I start with “Empty Compose Activity” in Android Studio.) ByteToDecimal() calls Byte(), which calls Bit() in a loop eight times to generate a “toggleable” button for each bit. It then calls Decimal() to generate a text composable which displays the current decimal value of the byte.

The Code

App State

/** Rick Regan, https://www.exploringbinary.com/ */

val byte = arrayOf(
    mutableStateOf(false),
    mutableStateOf(false),
    mutableStateOf(false),
    mutableStateOf(false),
    mutableStateOf(false),
    mutableStateOf(false),
    mutableStateOf(false),
    mutableStateOf(false)
)

var byteValue by mutableStateOf(0)

fun bitFlip(
    bitPosition: Int
) {
    var bit by byte[bitPosition]
    bit = !bit
    val bitValue = 2.0.pow(bitPosition.toDouble()).toInt()
    byteValue += if (bit) bitValue else -bitValue
}

The app represents a byte as eight individually mutable Booleans, a change to any of which will cause the corresponding button to be recomposed (redrawn). It represents the decimal value as a mutable integer, a change to which will recompose the text displaying the decimal value. The changes to this mutable state are initiated in the UI (button presses in Bit()) but made here in the function bitFlip() through a callback.

The app’s state lives outside of the composable functions so that it can survive configuration changes.

ByteToDecimal()

@Composable
fun ByteToDecimal() {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.fillMaxWidth()
    ) {
        Byte(byte, ::bitFlip)
        Spacer(modifier = Modifier.padding(top = 20.dp))
        Decimal("$byteValue")
    }
}

Byte()

Byte() creates eight buttons by calling the Bit() composable in a loop. It gives each button its bit position, the value of its mutable Boolean representation, and a reference to the bitFlip() function that reacts to the bit being toggled.

@Composable
fun Byte(
    byte: Array<MutableState<Boolean>>,
    bitFlip: (bitPosition: Int) -> Unit
) {
    Row {
        for (bitPosition in 7 downTo 0) {
            Bit(
                bitPosition,
                byte[bitPosition].value,
                bitFlip)
        }
    }
}

Bit()

Bit() displays a button with its current state (“0” or “1”), its place value equivalent in decimal above it, and its bit position below it. The buttons are styled to make it easier to differentiate between a “0” and a “1”.

@Composable
fun Bit(
    bitPosition: Int,
    bit: Boolean,
    bitFlip: (bitPosition: Int) -> Unit
) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = 2.0.pow(bitPosition.toDouble()).toInt().toString()
        )
        val colors = if (bit) {
            ButtonDefaults.buttonColors(
                backgroundColor = Color.White,
                contentColor = Color.Blue
            )
        } else {
            ButtonDefaults.buttonColors(
                backgroundColor = Color(240, 240, 240),
                contentColor = Color.Blue
            )
        }
        Button(
            onClick = { bitFlip(bitPosition) },
            modifier = Modifier
                .padding(2.dp)
                .size(45.dp),
            colors = colors,
            border = BorderStroke(1.dp, color = Color.Blue)
        ) {
            Text(
                text = if (bit) "1" else "0",
                modifier = if (!bit) Modifier.alpha(0.4f) else Modifier
            )
        }
        Text(
            text = bitPosition.toString(),
            color = Color.Gray
        )
    }
}

Decimal

Decimal() simply displays the decimal representation of the byte.

@Composable
fun Decimal(
    decimal: String
) {
    Text(
        text = decimal,
        Modifier
            .width(70.dp)
            .border(BorderStroke(1.dp, color = Color.Black))
            .padding(4.dp),
        textAlign = TextAlign.Center,
        color = Color(0, 180, 0),
        fontSize = 5.em
    )
}

Notes

  • I tried to demonstrate Compose concepts like mutable state, state hoisting, and unidirectional data flow. I also tried to follow all Kotlin and Compose coding conventions as I understand them at this stage.
  • I would have liked to have used property delegate syntax in declaring byte but I don’t know if that’s possible in an array (I do “var bit by byte[bitPosition]” in the code individually for each byte).
  • I could have made an array of “bitValue” instead of computing the powers of two each time, but I wanted to keep it simple.
  • I probably could have made Bit() and Decimal() more stateless by hoisting the styling information and hardcoded button labels (if those are considered state) but I’m not sure yet where that line is.
  • I use the line “if (!bit) Modifier.alpha(0.4f) else Modifier” but the “else Modifier” part seems a little clunky. I was looking for something like “Modifier.Unspecified”, similar to “Color.Unspecified”.
  • I had originally coded bitFlip() as a lambda but it was causing unnecessary (but ultimately harmless) recomposes. I’m investigating this on the #compose channel on Slack (login required).
  • My call to Decimal() passes the string representation of byteValue, which triggers recomposition as I want. However, I was expecting to have to pass byteValue itself, since it is the mutable state.
  • I did not include all the imports, but Android Studio should generate most of them (if you need them let me know and I’ll post them in the comments).
  • Compose is in alpha so the API may change. This article is based on alpha 11, using Android Studio 2020.3.1 Canary 5.

Feedback welcome!

Dingbat
RSS feed icon
RSS e-mail icon