Cannot set a character count limit on Textfield Android Jetpack Compose - android

I'm developing an Android app using Jetpack Compose.
I'd like to set a character count limit on Textfield, but if I try this code, users can input maximum 1201 characters(maxChar+1), and if users input 1201 characters(exceed maxChar), they cannot delete any letters.
#Composable
fun PostEdit(navController: NavController, content: String, id: String) {
var editedContent = remember { mutableStateOf("$content")}
val maxChar = 1200
...
OutlinedTextField(
value = editedContent.value,
onValueChange = { newValue: String ->
if (editedContent.value.length <= maxChar) {
editedContent.value = newValue
}
},
label = { Text("Content", fontSize = 20.sp, color = Brown) },
...
On a different view, I set a character count limit, too, and it is successful. The difference is, this time, there is no parameter in "var intro". This is the code.
#Composable
fun Signup(navController: NavController)
var intro by remember { mutableStateOf("") }
val maxChar = 100
...
OutlinedTextField(
value = intro,
onValueChange = {
if (it.length <= maxChar) {
intro = it
}
},
label = { Text("Introduction", fontSize = 20.sp, color = Brown) },
...
Could someone help me? Thank you.

The second one (intro) you are using the implicit it parameter not the intro state variable
onValueChange = { // <-- implicit `it`
if (it.length <= maxChar) {
intro = it
}
}
while on the first (editedContent) one you are using the state variable instead so when editedContent's length reaches 1200 and you typed another character it will still satisfy your onValueChange condition <=, making it 1201, and when you attempt to delete, the condition won't be satisfied anymore, no changes will happen to editedContent
onValueChange = { newValue: String -> // <-- use this
if (editedContent.value.length <= maxChar) {
editedContent.value = newValue
}
}
so do what you did on the second one to your first one and it will work
onValueChange = { newValue: String ->
if (newValue.length <= maxChar) {
editedContent.value = newValue
}
}

Related

Jetpack Compose delete TextField content with icon

I'm trying to make a custom TextField where the text gets deleted when the right icon is pressed. The problem with the following code is that the textFieldValue.text is a val, so it can't be reassigned.
So far the only solution I have found is to recompose the entire TextField sending "" as the text. Is there a better way to achieve this?
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver)
{ mutableStateOf(TextFieldValue("")) }
MyTextField(
text = textFieldValue,
onValueChange = {
textFieldValue = it
},
leftIconClickable = { /*Do nothing*/ },
rightIconClickable = { textFieldValue.text = "" }
)
(At this point this textField has almost the same code as the TextField of Jetpack Compose, the main difference being that it also receives 2 clickables for the icons)
Have your tried performing a copy()?
rightIconClickable = { textFieldValue = textFieldValue.copy(text = "") }
Here's a sample of a TextField using TextFieldValue with a simple Button where I copy() the current TextFieldValue instance, re-assigning it to the same variable, and it clears the TextField.
var textFieldValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(""))
}
Column {
TextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
}
)
Button(onClick = {
textFieldValue = textFieldValue.copy(text = "")}) {
}
}

TextField in Jetpack Compose [duplicate]

Is there any out of the box solution for limiting the character size in TextField's ?
I don't see any maxLength parameter like we had in XML.
With 1.x.y there isn't a built-in parameter.
You can use something like:
var text by remember { mutableStateOf(TextFieldValue("")) }
val maxChar = 5
TextField(
singleLine = true,
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
}
)
To display the counter text you can use something like:
val maxChar = 5
Column(){
TextField(
value = text,
onValueChange = {
if (it.length <= maxChar) text = it
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Text(
text = "${text.length} / $maxChar",
textAlign = TextAlign.End,
style = MaterialTheme.typography.caption,
modifier = Modifier.fillMaxWidth().padding(end = 16.dp)
)
}
The first answer to this question works fine, but it´s true that in some cases there is an error that when exceeding the number of characters allowed, the value of the texfield is cleared. This failure seems to be due to predictive text, because if predictive text is disabled in android, it does not happen. One solution I found for now as a workaround is to use focusManager to "limit writing".
First, we need to get the focus manager to control the focus on the screen.
We can do this, by adding this line inside our composable function:
val focusManager = LocalFocusManager.current
Then, in our TextField we can use the focusManager to avoid the user to write more than the maxChar limit. We can move the focus to the next element, clear the focus when the maxChar limit is exceeded or receive a lambda function and perform the action we want . That depends on us.
var text by remember { mutableStateOf(TextFieldValue("")) }
val maxChar = 10
TextField(
singleLine = true,
value = text,
onValueChange = {
// This line will take (in case the user try to paste a text from the clipboard) only the allowed amount of characters
text = it.take(maxChar)
if (it.length > maxChar){
focusManager.moveFocus(FocusDirection.Down) // Or receive a lambda function
}
}
)
In this way the user could never write more characters than what is established by the limit. Obviously, this is an alternative solution, which in my case solved my problem, now we have to wait to see if they add it natively
Trim the most recently inserted character according to selection, if the new string exceeds the length.
fun TextFieldValue.ofMaxLength(maxLength: Int): TextFieldValue {
val overLength = text.length - maxLength
return if (overLength > 0) {
val headIndex = selection.end - overLength
val trailIndex = selection.end
// Under normal conditions, headIndex >= 0
if (headIndex >= 0) {
copy(
text = text.substring(0, headIndex) + text.substring(trailIndex, text.length),
selection = TextRange(headIndex)
)
} else {
// exceptional
copy(text.take(maxLength), selection = TextRange(maxLength))
}
} else {
this
}
}
Usage:
val (phone, setPhone) = remember {
mutableStateOf(TextFieldValue())
}
PaddingTextField(
value = phone,
onValueChange = { newPhone ->
setPhone(newPhone.ofMaxLength(11))
}
)
You can use take function - here documentation
onValueChange = { onYearChanged(it.take(limitNum)) })
For example, if you will use it in function.
const val limitNum = 4
#Composable
fun YearRow(
modifier: Modifier = Modifier,
year: Int,
onYearChanged: (String) -> Unit,
) {
OutlinedTextField(
modifier = modifier,
value = if (year == 0) "" else "$year",
onValueChange = { onYearChanged(it.take(limitNum)) },
)
}
Another way to do this that could be considered more flexible is something like:
Text(
text = "A string with a lot of charsssssssssssssssssssssssssss"
modifier = Modifier.fillMaxWidth(.5f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
this will constraint the width with the fillMaxWidth bit and the height with the maxLines part. If both of those constraints are hit the text will overflow and the behavior for overflow can be specified
in this case once the text occupied half of the view or went more than one line it would end up something like A string with a lot of charsssss...

How to read the Semantic values of a Jetpack Compose TextField through UI tests?

I am new to Jetpack Compose testing and trying to figure out how to access the values of an OutlinedTextField to perform Instrumentation tests on them:
I can't figure out the syntax to access and check some of the values in the SemanticsNode of the EditFeild.
I am using the following Instrumentation Test:
#Test
fun NameTextField_LongInput_CompleteStatusAndLabelCorrect() {
composeTestRule.setContent {
ComposeTemplateTheme {
NameTextInput(name = "RandomName123", onNameInfoValid = { isComplete = it })
}
assertEquals(isComplete, true)
// This is accessing the label text
composeTestRule.onNodeWithText("Name").assertIsDisplayed()
//How do I access the Editable text?
//composeTestRule.onNodeWithEditableText("RandomName123") // How do I do something like this?!?!123
}
}
I would like to figure out how to access various items in this tree:
printToLog:
Printing with useUnmergedTree = 'false'
Node #1 at (l=0.0, t=110.0, r=1080.0, b=350.0)px
|-Node #2 at (l=48.0, t=158.0, r=1032.0, b=326.0)px
ImeAction = 'Default'
EditableText = 'RandomName123' // HOW DO I ACCESS THIS?!?! I want to confirm values of this?!?!
TextSelectionRange = 'TextRange(0, 0)'
Focused = 'false'
Text = '[Name]'
Actions = [GetTextLayoutResult, SetText, SetSelection, OnClick, OnLongClick, PasteText]
MergeDescendants = 'true'
Here is the complete Composable I am trying to test:
#Composable
fun NameTextInput(name: String, onNameInfoValid: (Boolean) -> Unit) {
// Name
val nameState = remember { mutableStateOf(TextFieldValue(name)) }
val nameString = stringResource(R.string.name)
val nameLabelState = remember { mutableStateOf(nameString) }
val isNameValid = if (nameState.value.text.length >= 5) {
nameLabelState.value = nameString
onNameInfoValid(true)
true
} else {
nameLabelState.value = stringResource(R.string.name_error)
onNameInfoValid(false)
false
}
OutlinedTextField(
shape = RoundedCornerShape(card_corner_radius),
value = nameState.value,
singleLine = true,
onValueChange = { if (it.text.length <= 30) nameState.value = it },
isError = !isNameValid,
label = { Text(nameLabelState.value) },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
capitalization = KeyboardCapitalization.Words
),
colors = customTextColors(),
modifier = Modifier
.fillMaxWidth()
.padding(horizfull_verthalf)
)
}
Great question, you can access the textfield via the tag of its modifier.
OutlinedTextField(
...
modifier = Modifier
.fillMaxWidth()
.padding(horizfull_verthalf)
.testTag("field")
)
Then access the text field with above tag, you can assert the result as below:
val value = composeTestRule.onNodeWithTag("field")
value.assertTextEquals("RandomName123") // verify value of textfield
for ((key, value) in value.fetchSemanticsNode().config) {
Log.d("AAA", "$key = $value") // access and print all config
if (key.name == "EditableText"){
assertEquals("RandomName123", value.toString())
}
}
Try and test success on my side.

Formatting numbers in compose TextField

I am trying to create a reusable NumberField component:
#Composable
fun NumberField(
value: Number?,
onNumberChange: (Number) -> Unit,
) {
TextField(
value = value?.toString() ?: "",
onValueChange = {
it.toDoubleOrNull()?.let { value ->
if (value % 1.0 == 0.0) {
onNumberChange(value.toInt())
} else {
onNumberChange(value)
}
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
To be used as:
#Composable
fun NumberContent() {
val number = remember { mutableStateOf<Number?>(null) }
NumberField(value = number.value) {
number.value = it
}
}
I would like the number to be an Int or Double depending on what the user is typing. What I have above works until you try to enter a decimal number, as it seems "5.", does not parse as double. I want to allow the user to type 5. and then fill in rest. As such I don't want to add a zero after decimal automatically because that might not be the next number they want to enter. Is this the best way to go about it? I know that I can just accept any text and then try to format the text they entered later as an int or double and make them fix it then, just thought it would be nice to bundle it all in the composable.
You can use something like:
TextField(
value = text,
onValueChange = {
if (it.isEmpty()){
text = it
} else {
text = when (it.toDoubleOrNull()) {
null -> text //old value
else -> it //new value
}
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Here is an implementation that handles all stated conditions while also exposing the state to parents.
#Composable
fun NumberField(
value: Number?,
onNumberChange: (Number?) -> Unit,
) {
val number = remember { mutableStateOf(value) }
val textValue = remember(value != number.value) {
number.value = value
mutableStateOf(value?.toDouble()?.let {
if (it % 1.0 == 0.0) {
it.toInt().toString()
} else {
it.toString()
}
} ?: "")
}
val numberRegex = remember { "[-]?[\\d]*[.]?[\\d]*".toRegex() }
// for no negative numbers use "[\d]*[.]?[\d]*"
TextField(
value = textValue.value,
onValueChange = {
if (numberRegex.matches(it)) {
textValue.value = it
number.value = it.toDoubleOrNull()
onNumberChange(number.value)
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
An example usage is shown below.
#Composable
fun DemoUsage() {
Column {
val number = remember { mutableStateOf<Number?>(null) }
NumberField(value = number.value) {
number.value = it
}
Button(onClick = { number.value = number.value?.toDouble()?.plus(1) }) {
Text("Increment")
}
}
}

TextField with Kotlin StateFlow

I'd like to have a TextField bound to a MutableStateFlow that comes from a view model. This is how I set it up:
#Composable
fun MyTextField(textFlow: MutableStateFlow<String>) {
val state = textFlow.collectAsState(initial = "")
TextField(
value = TextFieldValue(state.value),
onValueChange = { textFlow.value = it.text },
label = { Text(text = "Label") }
)
}
When I type something into the text field, it behaves really strangely. For example, if I type 'asd', it ends up with 'asdasa'. How can I update textFlow.value without messing up with the text field?
This error is caused by the usage of TextFieldValue with Flow.
To fix this, set the value of the TextField to just state.value and then on text change set the value with textFlow.value = it.
#Composable
fun MyTextField(textFlow: MutableStateFlow<String>) {
val state = textFlow.collectAsState(initial = "")
TextField(
value = state.value,
onValueChange = { textFlow.value = it },
label = { Text(text = "Label") }
)
}

Categories

Resources