Nuxtstop

For all things nuxt.js

Keyboard handling in Jetpack Compose

Keyboard handling in Jetpack Compose
26 1

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:

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

Screenshot of KeyboardHandlingDemo1()

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

KeyboardHandlingDemo1() with a visible soft keyboard

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 ...

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.

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

android:windowSoftInputMode="adjustResize"
Enter fullscreen mode Exit fullscreen mode

to your <activity /> tag.

KeyboardHandlingDemo1() in a properly resized window

Multiple text fields

Let's look at another Compose hierarchy:

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

KeyboardHandlingDemo2() with open soft keyboard

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():

keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
  onNext = {
    focusManager.moveFocus(FocusDirection.Down)
  }
),
Enter fullscreen mode Exit fullscreen mode

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.

val focusManager = LocalFocusManager.current
Enter fullscreen mode Exit fullscreen mode

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:

listState.animateScrollToItem(i)
Enter fullscreen mode Exit fullscreen mode

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

coroutineScope.launch {
  listState.animateScrollToItem(i)
}
Enter fullscreen mode Exit fullscreen mode

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

val coroutineScope = rememberCoroutineScope()
Enter fullscreen mode Exit fullscreen mode

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

Let's look at the code:

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

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