DEV Community

loading...
Cover image for Implementing a number pad in Jetpack Compose

Implementing a number pad in Jetpack Compose

tkuenneth profile image Thomas Künneth ・5 min read

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. A ready-to-use version can be downloaded from the Play Store.

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

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

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:

<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" />
Enter fullscreen mode Exit fullscreen mode

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.

class NewMainActivity : ComponentActivity() {

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

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:

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
)
Enter fullscreen mode Exit fullscreen mode

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

@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
      )
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

val callback = { text: String ->
  handleButtonClick(text, input, output, lastOp, result)
Enter fullscreen mode Exit fullscreen mode

As this is business logic I will not show you handleButotnClicked() here but please do take a look at the source.

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

@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
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)

Forem Open with the Forem app