Keyboard handling in Jetpack Compose
Tags: android, jetpackcompose, kotlin
Published: 2022-03-19 11:27:12

Entering data is an important task in many apps. On devices with no physical keyboard (the vast majority in Android land) a so-called *soft(ware) keyboard* handles user input. Now, you may be wondering why we need to talk about these virtual peripherals at all. Shouldn't the operating system take care? I mean, in terms of user interface, the app expresses its desire to allow user input by showing and configuring an editable text field. What else needs to be done? This article takes a closer look at how Jetpack Compose apps interact with the keyboard.

Let's start with a simple Compose hierarchy:

```kotlin
@Composable
fun KeyboardHandlingDemo1() {
  var text by remember { mutableStateOf(TextFieldValue()) }
  Column(
    modifier = Modifier.fillMaxSize(),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Bottom
  ) {
    Box(
      modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .background(color = MaterialTheme.colors.primary)
        .weight(1.0F),
      contentAlignment = Alignment.BottomCenter
    ) {
      Text(
        modifier = Modifier.padding(bottom = 16.dp),
        text = stringResource(id = R.string.app_name),
        color = MaterialTheme.colors.onPrimary,
        style = MaterialTheme.typography.h5
      )
    }
    TextField(modifier = Modifier.padding(bottom = 16.dp),
      value = text,
      onValueChange = {
        text = it
      })
  }
}
```

![Screenshot of KeyboardHandlingDemo1()](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23z1mh2w9jf44rw6zfec.png)

Looks good, right? Now, let's see what happens if the text field gets focus.


![KeyboardHandlingDemo1() with a visible soft keyboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gas089tw6ayjwm9idgjh.png)

This surely doesn't look terrible, but it isn't great, either. As the text field will likely have the attention of the user, it should be visible fully, right? Here, it's important to understand how the soft keyboard interacts with the activity and the window the activity is displayed in. There has almost always (since API level 3) been a manifest attribute for this, `windowSoftInputMode`. It belongs to `<activity />`. 

[The attribute ...](https://developer.android.com/guide/topics/manifest/activity-element#wsoft)

> controls how the main window of the activity interacts with the window containing the on-screen soft keyboard.

There are two main aspects:

- Should the soft keyboard be visible when the activity becomes the focus of user attention?
- Which adjustment should be made to the activity's main window when a part of the window is covered by the soft keyboard?

In this article, I'll focus on the latter one. For a general introduction, please refer to [Handle input method visibility](https://developer.android.com/training/keyboard-input/visibility).

Now, let's look at adjustment-related values.

`adjustUnspecified` is the default setting for the behavior of the main window. The doc says:

> It is unspecified whether the activity's main window resizes to make room for the soft keyboard, or whether the contents of the window pan to make the current focus visible on-screen. The system will automatically select one of these modes depending on whether the content of the window has any layout views that can scroll their contents. If there is such a view, the window will be resized, on the assumption that scrolling can make all of the window's contents visible within a smaller area.

`adjustResize`:

> The activity's main window is always resized to make room for the soft keyboard on screen.

`adjustPan`:

> The activity's main window is not resized to make room for the soft keyboard. Rather, the contents of the window are automatically panned so that the current focus is never obscured by the keyboard and users can always see what they are typing. This is generally less desirable than resizing, because the user may need to close the soft keyboard to get at and interact with obscured parts of the window.

If you recall the last screenshot, the window obviously is **not** resized. So, ... 

> The system will automatically select one of these modes depending on whether the content of the window has any layout views that can scroll their contents.

... seems to not work well for Compose apps. Which is certainly not surprising, as the root view of a Compose hierarchy being displayed using `setContent { ... }` is `ComposeView`, which extends `AbstractComposeView`, which in turn extends `ViewGroup` (which can't scroll).

So, the *fix* is simple: just add

```xml
android:windowSoftInputMode="adjustResize"
```

to your `<activity />` tag.


![KeyboardHandlingDemo1() in a properly resized window](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uciwamofnw4h111tlm9p.png)

### Multiple text fields

Let's look at another Compose hierarchy:

```kotlin
@Composable
fun KeyboardHandlingDemo2() {
  val states = remember {
    mutableStateListOf("1", "2", "3", 
        "4", "5", "6", "7", "8", "9", "10")
  }
  val listState = rememberLazyListState()
  LazyColumn(
    state = listState,
    modifier = Modifier.fillMaxSize(),
    horizontalAlignment = Alignment.CenterHorizontally
  ) {
    itemsIndexed(states) { i, _ ->
      OutlinedTextField(value = states[i],
        modifier = Modifier.padding(top = 16.dp),
        onValueChange = {
          states[i] = it
        },
        label = {
          Text("Text field ${i + 1}")
        })
    }
  }
}
```

![KeyboardHandlingDemo2() with open soft keyboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t4kbkvyjk6me10xmo8np.png)

The user interface contains quite a few editable text fields. However, with the above implementation, the user cannot move to the next field using the soft keyboard, but must click and scroll. Fortunately, we can achieve this easily using Compose keyboard actions and options. The following lines are added to the call to `OutlinedTextField()`:

```kotlin
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
  onNext = {
    focusManager.moveFocus(FocusDirection.Down)
  }
),
```

We configure the soft keyboard to display a special *Next* key (`ImeAction.Next`) and use a `FocusManager` (which belongs to package `androidx.compose.ui.focus`) inside the `onNext` callback of `KeyboardActions` to navigate (move the focus) to the (vertically) next text field.

```kotlin
val focusManager = LocalFocusManager.current
```

Cool, right? There is one more thing we need to do, though. Our text fields belong to a scrollable list. Moving focus does not change the portion of the list that is currently visible. Here's how to do that:

```kotlin
listState.animateScrollToItem(i)
```

`animateScrollToItem()` is a suspend function, so it should be called from a coroutine or another suspend function.

```kotlin
coroutineScope.launch {
  listState.animateScrollToItem(i)
}
```

Finally, to get a coroutine scope in a composable function, you can use `rememberCoroutineScope()`:

```kotlin
val coroutineScope = rememberCoroutineScope()
```

There's one more thing I'd like to show you: how to close the software keyboard.

### Showing and hiding the soft keyboard

The following screenshot shows my `KeyboardHandlingDemo3()` composable function. It allows the user to enter a number and computes its square after the *Calculate* button or the special *Done* key on the soft keyboard was pressed. What you can't see on the screenshot: the soft keyboard is closed. This may be desirable to again show the complete user interface after the data has been input.

![The KeyboardHandlingDemo3() example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lb0rgsnjz85colr2b4dd.png)

Let's look at the code:

```kotlin
@ExperimentalComposeUiApi
@Composable
fun KeyboardHandlingDemo3() {
  val kc = LocalSoftwareKeyboardController.current
  var text by remember { mutableStateOf("") }
  var result by remember { mutableStateOf("") }
  val callback = {
    result = try {
      val num = text.toFloat()
      num.pow(2.0F).toString()
    } catch (ex: NumberFormatException) {
      ""
    }
    kc?.hide()
  }
  Column(
    modifier = Modifier.fillMaxSize(),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
  ) {
    Row {
      TextField(modifier = Modifier
        .padding(bottom = 16.dp)
        .alignByBaseline(),
        keyboardOptions = KeyboardOptions(
          keyboardType = KeyboardType.Number,
          imeAction = ImeAction.Done
        ),
        keyboardActions = KeyboardActions(
          onDone = {
            callback()
          }
        ),
        value = text,
        onValueChange = {
          text = it
        })
      Button(modifier = Modifier
        .padding(start = 8.dp)
        .alignByBaseline(),
        onClick = {
          callback()
        }) {
        Text(stringResource(id = R.string.calc))
      }
    }
    Text(
      text = result,
      style = MaterialTheme.typography.h4
    )
  }
}
```

The computation takes place in the `callback` lambda. Here, the soft keyboard is closed, too, by invoking `hide()` on a `LocalSoftwareKeyboardController` instance. Please note that this API is experimental and may change in future Compose versions.

We configure a number pad with a *Done* key by passing `keyboardType = KeyboardType.Number` and `imeAction = ImeAction.Done` to `KeyboardOptions()`. The `callback` lambda is invoked from the `onClick` callback of the button and inside `onDone`, which belongs to `KeyboardActions()`.

### Conclusion

In this article I showed you how to interact with the soft keyboard in Compose apps. Did I miss something? Would you like a follow up? Kindly share your thoughts in the comments.

---

[Source](https://github.com/tkuenneth/android-demos/tree/master/compose/KeyboardHandlingDemo)