3

Hello!

I have an issue with TextField in Jetpack Compose, Android.
We have a sequence of screens where each screen has TextField, and I want to keep the keyboard open when the screen is changed to the next or previous one. But now when I change the screen, the keyboard is closed and opens what looks terribly bad to the user.

Video: https://youtube.com/shorts/RmSPGT2Rteo


Example

In original, I have separate ViewModels connected to these screens, a lot of other components on them and navigation library to make the navigation concise. This is a very simplified sample of the issue I suffer from:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        var screenIndex by remember { mutableStateOf(0) }

        when (screenIndex) {
            0 -> Screen(0) { screenIndex = 1 }
            1 -> Screen(1) { screenIndex = 0 }
        }
    }
}

@Composable
fun Screen(
    index: Int,
    onButtonClick: () -> Unit,
) {
    Column(
        modifier = Modifier.fillMaxSize().imePadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        val focusRequester = remember { FocusRequester() }

        LaunchedEffect(Unit) {
            focusRequester.requestFocus()
        }

        var value by remember { mutableStateOf("$index") }
        TextField(
            modifier = Modifier.focusRequester(focusRequester),
            value = value,
            onValueChange = { value = it },
        )

        Button(onClick = onButtonClick) {
            Text("Change screen")
        }
    }
}

What I've tried to do

I've read the source code of the CoreTextField and learnt the following: There are special function which disposes the TextInputSession when TextField is removed from the composition.

Source from CoreTextField.kt, line 316. Compose Foundation version is 1.3.1

// Hide the keyboard if made disabled or read-only while focused (b/237308379).
if (enabled && !readOnly) {
    // TODO(b/230536793) This is a workaround since we don't get an explicit focus blur event
    //  when the text field is removed from the composition entirely.
    DisposableEffect(state) {
        onDispose {
            if (state.hasFocus) {
                onBlur(state)
            }
        }
    }
}

Also, I've tried the following things:

  1. Add the delay before opening the keyboard
  2. Disable TextField before changing the screen (partially works but in my project screen navigation happens in the ViewModel level and it's impossible to synchronize that processes)
  3. Use InputMethodManager to open the keyboard, especially it's toggleSoftInput method but it's deprecated.

How can I keep the keyboard opened and move focus to the new TextField when the screen is changed?

Alexey
  • 56
  • 6
  • just as I suggestion - try to show keyboard in the scope of activity, not in the scope of composable – Sergei Mikhailovskii Jan 14 '23 at 12:21
  • it feels like there is a bad design ... why do you need to move to a different screen why can't you just put the next input in the same screen? and make a form? – Dolev Dublon Jan 18 '23 at 21:56
  • 1
    @DolevDublon It doesn't matter whether I have one or several composable screens. When the TextField is removed from the composition the keyboard is closed. It can be the same screen with several fields and when the focused field is removed then the keyboard is closed. The first solution is to move the focus to the next field and remove the previous field then, but this solution doesn't fit our design cause we could have a server-driven form with a lot of steps inside – Alexey Jan 19 '23 at 10:23
  • https://stackoverflow.com/a/1510005/10147641 or https://stackoverflow.com/a/47826869/10147641 I hope you must have tried this. – Mukund Jogi Jan 20 '23 at 06:35
  • Hi Alexey if you can change the implementation to be on the same screen, then there are some on youtube search for "jetpack compose form" – Dolev Dublon Jan 22 '23 at 18:59

1 Answers1

4

You can trick the IME by starting an input session as soon as the screen is displayed. Doing this, the IME will not have the time to close and will stay opened.

Starting an input session

Starting an input session is achieved by using LocalTextInputService.current which retained a reference of the following class TextInputService.

Using the TextInputService.startInput(...) method will launch the session.

The following code will launch it :

 val textInputService = LocalTextInputService.current

 textInputService?.startInput(
    value = TextFieldValue(""),
    imeOptions = ImeOptions.Default,
    onEditCommand = {},
    onImeActionPerformed = {}
 )

Wrapping inside a modifier extension

To have a cleaner code, we can wrap this piece of code inside a modifier extension to allow reusability.

fun Modifier.startInputSession() = composed {
    val textInputService = LocalTextInputService.current

    textInputService?.startInput(
        value = TextFieldValue(""),
        imeOptions = ImeOptions.Default,
        onEditCommand = {},
        onImeActionPerformed = {}
    )
    
    this
}

Use of composed {} is mandatory because we need to access LocalTextInputService.current

Example

Using it is simple, you just need to wrap your screens inside a box who use the Modifier.startInputSession() extension method

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
         Box(
                modifier = Modifier.startKeyboardSession()
            ) {
                var screenIndex by remember { mutableStateOf(0) }

                when (screenIndex) {
                    0 -> Screen(0) { screenIndex = 1 }
                    1 -> Screen(1) { screenIndex = 0 }
                }
            }
    }
}

Issues

Don't forget that we are faking an input session EACH time the screen is recomposed. So be sure that each time you are navigating you are calling the startInputSession() method again, otherwise it will not work.

Jolan DAUMAS
  • 1,078
  • 1
  • 5
  • 9