Implementing a number pad in Jetpack Compose
Tags: android, kotlin, jetpackcompose
Published: 2021-04-24 13:16:03

The best way to get familiar with a new technology is to use it. In this short piece I will be transforming a `View`-based layout to Jetpack Compose. It is taken from a small app I wrote quite some time ago. It's certainly open source, you can find it at [GitHub](https://github.com/tkuenneth/timecalculator). A ready-to-use version can be downloaded from the [Play Store](https://play.google.com/store/apps/details?id=com.thomaskuenneth.zeitrechner&hl=de&gl=US). 

The idea of the app is to add or subtract times, but that's not important as we will be focussing on the ui. The `View`-based version looks like this:

![A time calculator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qxlrqfiylu1950zvet6b.png)

We have three sections:

- An area where the (portion of) the currently entered time is shown
- A scrollable history of the times, operations and results
- A number pad

The number pad is somewhat special as it has no keys to multiply or divide, just the ones needed to add and subtract times. As you can see in the screenshot below, the layout is based upon `RelativeLayout` and `TableLayout`. Did I mention that the app has been written quite some time ago? 🤣

![The layout file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wakntxosufdobmuixwsn.png)

`TableLayout` has been there sine api level 1. Its beauty lies in easily grasping how its children will be laid out - as a table. Just as we would expect from tables we can stretch cells to span several columns. We need this for the 0 key. It's wider than the others.

The problem with such a layout is that  we need to specify a lot. We need 4 rows and each one starts like this:

```xml
<TableRow
  android:layout_width="match_parent"
  android:layout_height="wrap_content">

  <Button
    style="?android:attr/buttonBarButtonStyle"
    android:id="@+id/b7"
    android:onClick="handleButtonClick"
    android:text="@string/seven" />

  <Button
    style="?android:attr/buttonBarButtonStyle"
    android:id="@+id/b8"
    android:onClick="handleButtonClick"
    android:text="@string/eight" />
```

Three rows have four buttons, one has two. We might have created a separate layout for the four button row and include it three times in the main layout but then we would need to set the label of the buttons in code. So if we shrink the layout file we need to compensate this in code.

But let's leave the old way behind and concentrate on Jetpack Compose, shall we?

Right.

```kotlin
class NewMainActivity : ComponentActivity() {

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
      Scaffold(
        topBar = {
          TopAppBar(title = {
            Text(stringResource(id = R.string.app_name))
          })
        }
      ) {
        Content()
      }
    }
  }
}
```

How you populate your `Scaffold` depends on the complexity of your app. In any case I suggest to create a toplevel composable like `Content`.

Before I show you the code of mine, let's recap what our ui contains:

- An area where the (portion of) the currently entered time is shown
- A scrollable history of the times, operations and results
- A number pad

Let's start with the number pad. In the end we will be putting the keys in `Row()`s and wrap them in a `Column()`. But let's refraim from just accumulating `Button()`s, because in doing so we would be inheriting the issues from my old implementation. What I suggest instead is taking advantage of composables being part of the code. Take a look:

```kotlin
MyRow(
  listOf("7", "8", "9", "CE"),
  listOf(0.25f, 0.25f, 0.25f, 0.25f),
  callback
)
MyRow(
  listOf("4", "5", "6", "-"),
  listOf(0.25f, 0.25f, 0.25f, 0.25f),
  callback
)
MyRow(
  listOf("1", "2", "3", "+"),
  listOf(0.25f, 0.25f, 0.25f, 0.25f),
  callback
)
MyRow(
  listOf("0", ":", "="),
  listOf(0.5f, 0.25f, 0.25f),
  callback
)
```

`MyRow()` receives three parameters, a list of keys, a callback and a list of `Float` values. Why is that?

```kotlin
@Composable
fun MyRow(
  texts: List<String>,
  weights: List<Float>,
  callback: (text: String) -> Any
) {
  Row(modifier = Modifier.fillMaxWidth()) {
    for (i in texts.indices) {
      MyButton(
        text = texts[i],
        modifier = Modifier.weight(weights[i]),
        callback = callback
      )
    }
  }
}
```

Each row consists of either three or four buttons. Generally speaking we want all buttons to share the same size, but not the 0. Using the `weight` modifier we can easily controle this. The weight in a row sums up to `1.0f`, so 4 normal sized buttons get `0.25f` each. The row containing 0 has only three buttons, so the weights are `0.5f`, `0.25f` and `0.25f`.

You may be asking why I wrap a button in yet another composable.

```kotlin
@Composable
fun MyButton(
  text: String,
  callback: (text: String) -> Any,
  modifier: Modifier = Modifier
) {
  Button(
    modifier = modifier
      .padding(4.dp),
    onClick = {
      callback(text)
    }
  ) {
    Text(text)
  }
}
```

First, it is easy to customize its look. My code does not do that, besides setting the `padding()`, but we could do more like changing fonts and colors. What's more important: the callback it recieves is not immediately passed as `onClick()` but invoked with a parameter: the button text. This way my composable cann tell the callback *which* button it represents.

```kotlin
val callback = { text: String ->
  handleButtonClick(text, input, output, lastOp, result)
```

As this is business logic I will not show you `handleButotnClicked()` here but please do take a look at the [source](https://github.com/tkuenneth/timecalculator).

To close this article, let's look at `Content()`:

```kotlin
@Preview
@Composable
fun Content() {
  val input = remember { mutableStateOf("") }
  val output = remember { mutableStateOf("") }
  val result = remember { mutableStateOf(0) }
  val lastOp = remember { mutableStateOf("") }
  val state = rememberScrollState()
  val scope = rememberCoroutineScope()
  val callback = { text: String ->
    handleButtonClick(text, input, output, lastOp, result)
    scope.launch {
      state.animateScrollTo(state.maxValue)
    }
  }
  Column(
    modifier = Modifier
      .fillMaxSize()
      .padding(16.dp)
  ) {
    Zeile1(input.value)
    Spacer(modifier = Modifier.height(16.dp))
    Text(
      text = output.value,
      modifier =
      Modifier
        .fillMaxWidth()
        .weight(1.0f)
        .verticalScroll(state = state),
      style = MaterialTheme.typography.body1
    )
    Spacer(modifier = Modifier.height(16.dp))
    MyRow(
      listOf("7", "8", "9", "CE"),
      listOf(0.25f, 0.25f, 0.25f, 0.25f),
      callback
    )
    MyRow(
      listOf("4", "5", "6", "-"),
      listOf(0.25f, 0.25f, 0.25f, 0.25f),
      callback
    )
    MyRow(
      listOf("1", "2", "3", "+"),
      listOf(0.25f, 0.25f, 0.25f, 0.25f),
      callback
    )
    MyRow(
      listOf("0", ":", "="),
      listOf(0.5f, 0.25f, 0.25f),
      callback
    )
  }
}
```

It shows you the rest of the ui. Please note how you can use `animateScrollTo()` to scroll to the end of a longer text.

# Takeaways

- Remember that composables should not contain business logic
- Take advantage from composables being code by using them in loops
- Whenever possible they should be reusable and stateless, so if applicable pass parameters to them instead of calculating values in the composable 
- If you need state that is used somewhere else, too, pass the state to the composable

What are your thoughts on this? Please share your impressions in the comments.