A focused TextField will wrongly intercept OnBackPress event in Jetpack Compose

Julien Bouffard
2 min readNov 1, 2022

--

Photo by Denny Müller on Unsplash

I encountered a problem recently on jetpack compose foundation 1.2.0.

On a compose screen when requesting focus on a TextField, pressing system back button was not working as expected.

  • First on back press, the keyboard close (that’s expected)
  • Second on back press, the TextField loses the cursor (that’s not what I want)
  • Third on back press, my BackHandler is finally called

There was an issue about it on Google which is supposed to be fixed: https://issuetracker.google.com/issues/192433071

It might be fixed in some cases but not every time apparently.

I was expecting a behavior like on a xml view edit text, to press back only twice to reach my onBackPressedDispatcher.

What happens actually?

The problem resides in CoreTextField which is inherited eventually by TextField.

CoreTextField will intercept any onBackPress in .previewKeyEventToDeselectOnBack and .then(textKeyInputModifier). Ultimately, both these modifiers will return false to propagate the event but the field hasn’t lost the focus and the key event isn’t propagated to the BackHandler.

package androidx.compose.foundation.text@Composable
@OptIn(InternalFoundationTextApi::class, ExperimentalFoundationApi::class)
internal fun CoreTextField(....) {
....
val decorationBoxModifier = modifier
.then(focusModifier)
.previewKeyEventToDeselectOnBack(state, manager)
.then(textKeyInputModifier)
.textFieldScrollable(scrollerPosition, interactionSource, enabled)
.then(pointerModifier)
.then(semanticsModifier)
.onGloballyPositioned {
state.layoutResult?.decorationBoxCoordinates = it
}
// Focus
val focusModifier = Modifier.textFieldFocusModifier(
enabled = enabled,
focusRequester = focusRequester,
interactionSource = interactionSource
) {
if (state.hasFocus == it.isFocused) {
return@textFieldFocusModifier
}
state.hasFocus = it.isFocused

if (textInputService != null) {
notifyTextInputServiceOnFocusChange(
textInputService,
state,
value,
imeOptions
)

...
}
if (!it.isFocused) manager.deselect()
}

In addition to KeyEvent interception, CoreTextField will be resetting the field.

textFieldFocusModifier will take care of resetting the TextField and if (!it.isFocused) manager.deselect() will be setting the cursor to None.

androidx.compose.foundation.text.selection.TextFieldSelectionManagerinternal fun deselect(position: Offset? = null) {
if (!value.selection.collapsed) {
...
}

val selectionMode = if (position != null && value.text.isNotEmpty()) {
HandleState.Cursor
} else {
HandleState.None
}
setHandleState(selectionMode)
hideSelectionToolbar()
}

Once the TextField has been reset, pressing system back button will trigger our BackHandler as expected.

However it should not deal with resetting the field and the cursor in the first place but pass the event to our BackHandler instead. The modifier took the place in the queue and did not gave it back to our BackHandler.

What can we do to circumvent this problem?

Adding our own interceptor to the Modifier will force the field to completely lose focus and then propagate the event which will be consumed by our BackHandler.

val focusManager = LocalFocusManager.currentTextField(
modifier = Modifier
.onKeyEvent {
if (it.nativeKeyEvent.action == KeyEvent.ACTION_DOWN &&
it.nativeKeyEvent.keyCode == KEYCODE_BACK
) {
focusManager.clearFocus()
}
false
}

--

--