Here is my code:
Components.kt
package com.example.jettipapp.components
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.sp
@Composable
fun InputField(
modifier: Modifier = Modifier,
valueState: MutableState<String>,
labelId: String,
enabled: Boolean,
isSingleLine: Boolean,
keyboardType: KeyboardType = KeyboardType.Number,
imeAction: ImeAction = ImeAction.Next,
onAction: KeyboardActions = KeyboardActions.Default
) {
OutlinedTextField(
value = TextFieldValue(annotatedString = buildAnnotatedString { valueState.value }),
onValueChange = { currentValue: TextFieldValue -> valueState.value =
currentValue.toString()
},
modifier = modifier,
label = { Text(labelId) },
leadingIcon = { Icon(imageVector = Icons.Rounded.AttachMoney, contentDescription = "Money Icon") },
singleLine = isSingleLine,
textStyle = TextStyle(fontSize = 20.sp, color = MaterialTheme.colorScheme.onBackground),
enabled = enabled,
keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction),
keyboardActions = onAction,
readOnly = false
)
}
and here is
MainActivity.kt
package com.example.jettipapp
import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import com.example.jettipapp.ui.theme.JetTipAppTheme
import com.example.jettipapp.components.InputField
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
App {
MainContent()
}
}
}
}
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun App(content: @Composable () -> Unit) {
JetTipAppTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
content()
}
}
}
@Composable
fun MainContent() {
val totalBill = remember {
mutableStateOf("")
}
val vaild = remember(totalBill.value) {
totalBill.value.trim().isNotEmpty()
}
val keyboardController = LocalSoftwareKeyboardController.current
Surface(
modifier = Modifier
.padding(2.dp)
.height(500.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(10.dp),
border = BorderStroke(2.dp, Color.LightGray)
) {
Column() {
InputField(
valueState = totalBill,
labelId = "Enter Bill:",
enabled = true,
isSingleLine = true,
onAction = KeyboardActions {
if (!vaild) return@KeyboardActions
keyboardController?.hide()
}
)
}
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
App {
MainContent()
}
}
My goal is to make a text field with the label Total Bill
and the dollar icon beside it. The keyboard will be only numeric values. When the blue next button is clicked, it should check if there is something added in the text field and close the keyboard if there is, otherwise make it stay as-is.
The ui works fine, the TextField looks great! But, first of all, the keyboard is not straight up appearing, you have to manually bring it up.
And even when I do bring it up, I cannot type with it.
But I can still hide the keyboard even though the text field appears blank...
Just in case It is needed, I am using API 36 with Android 16 "Baklava". And the device is Google Pixel 9a
(The following answer only addresses why the input isn't displayed. Opening and closing the keyboard is unrelated to this and should be asked as a new question. Please make sure to better describe what the issue is because I couldn't reproduce it. Make also sure to explain in more detail what the expected behavior is and why the default behavior doesn't suffice. I don't see how closing the keyboard while typing can ever be helpful.)
There are many issues in your code, but the one eventually preventing any input to be displayed is caused by an improper use of the buildAnnotatedString
builder:
buildAnnotatedString {
valueState.value
}
The lambda is declared as (Builder).() -> Unit
, which means the return value (valueState.value
) is discarded. Instead, you want to append the string to the string builder, like this:
buildAnnotatedString {
append(valueState.value)
}
This will, at least, let the text field display the input.
What still doesn't work is retaining the current cursor position. It will always stay at the beginning of the input so new characters are added to the beginning and not the end.
The reason for that is that, on recomposition, you create a new TextFieldValue
object from scratch, without specifying its selection
parameter. The cursor will therefore never be moved.
The solution to this is to re-use the TextFieldValue object that the OutlinedTextField's onValueChange
parameter provides. You currently use this:
onValueChange = { currentValue: TextFieldValue ->
valueState.value = currentValue.text
},
currentValue
already contains the updated text and the correct cursor position, so you want to feed this object back into the OutlinedTextField's value
parameter instead. This is usually done like this:
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
OutlinedTextField(
value = textFieldValue,
onValueChange = { currentValue: TextFieldValue ->
textFieldValue = currentValue
},
// ...
)
in onValueChange you only update the local textFieldValue which triggers a recomposition for OutlinedTextField only.
This won't update valueState.value
anymore, though. But before looking any further, let's first assess the current situation with OutlinedTextField: Although you constructed the TextFieldValue earlier with an annotated string, you never used any of the features of an annotated string. And when you just use simple strings and don't want to modify the selected text, it is easier to not use TextFieldValue at all:
var text by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = text,
onValueChange = {
text = it
},
// ...
)
With this you only ever handle String objects, not TextFieldValues.
Now back to the issue that valueState
isn't updated anymore. This is nothing your InputField should actually concern itself with, though. Your InputField has this parameter:
valueState: MutableState<String>,
You should never pass MutableState objects to composables. Instead, only pass their value and provide another parameter with a lambda that is called on change:
value: String,
onChange: (String) -> Unit,
But you do not even seem to need the value
(or valueState
) parameter at all, so just remove it entirely. That also alleviates you from the prior problem of how to update valueState.value
.
Instead, just invoke the new onChange
in the OutlinedTextField's onValueChange:
onValueChange = {
text = it
onChange(it)
},
Then you can clean up MainContent as well: Completely remove totalBill
, it isn't needed anymore. Fix the typo in vaild
and declare it like this instead:
var valid by rememberSaveable { mutableStateOf(false) }
Then you can call InputField like this:
InputField(
onChange = { valid = it.trim().isNotEmpty() },
labelId = "Enter Bill:",
enabled = true,
isSingleLine = true,
onAction = KeyboardActions {
if (!valid) return@KeyboardActions
keyboardController?.hide()
},
)