Reputation: 66
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
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")
}
}
}
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:
How can I keep the keyboard opened and move focus to the new TextField when the screen is changed?
Upvotes: 3
Views: 844
Reputation: 1374
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 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 = {}
)
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
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 }
}
}
}
}
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.
Upvotes: 4