Compose TextField clears value on gaining focus - android

I have a Composable function which displays 2 TextFields. Here's my code:
fun CreateEntryItem() {
var wordA by remember { mutableStateOf("") }
var wordB by remember { mutableStateOf("") }
Column {
Row {
TextField(
value = wordA,
onValueChange = { wordA = it },
enabled = true,
modifier = Modifier.weight(1f)
)
TextField(
value = wordB,
onValueChange = { wordB = it },
enabled = true,
modifier = Modifier.weight(1f)
)
}
}
}
When I give focus to TextField A, I can type and the value of wordA updates correctly.
Here's the weird behaviour:
I then give focus to TextField B. I then give focus back to TextField A. When I start typing, instead of TextField A inserting / appending characters at the cursor position in the existing text, it completely clears the existing text (as set pre-focus), and 'starts afresh'. That is to say, each TextField only remembers text entered in the current 'focus session'.
Am I doing this wrong? Or is this a bug in Compose? I reproduced this behaviour on both 1.0.0-beta07 and 1.0.0-beta08.

Turns out this isn't a Compose bug per se, but an emulator issue. Running the exact same code on a real device didn't run into this issue.
The emulator that encountered this issue was API 30 running on macOS Big Sur. I haven't tested whether other systems are also affected by this emulator bug.

Related

Jetpack Compose: Focus Styling

I am making custom components and with my custom components I am required to cover the focus state.
At the top of my component hierarchy I am tracking focus with this.
val focused = remember { mutableStateOf(false) }
val focusModifier = modifier.onFocusEvent {
focused.value = it.hasFocus || it.isFocused
}
Component(modifier = focusModifier, focused = focused.value)
The component is basically this:
#Composable
fun Component(
modifier: Modifier = Modifier,
focused: Boolean = false
) {
...
val colorStuff = if(focused) focusColors else otherColors
var focusModifier = modifier
if(focused) {
focusModifier = modifier.border(BorderStroke(2.dp, Color.Red)).padding(16.dp)
}
NextComponent(
focusModifier,
colorStuff,
etc
)
}
If I leave the code with colorStuff and focused without the focusModifier code the focus state is done correctly and the colors for the component change appropriately. But when I add the focusModifier code and do a border and padding the focus state will trigger, but then instantly be lost. I'm assuming this is because the addition of modifier code changes the build order of the component and makes it discard focus. But that doesn't make full sense either.
I essentially need to add borders/shadows around components when they are focused so this will be something I have to do multiple times. Right now I can't get it done once. Any idea what I need to do to overcome this?
This is not an answer to your question, but I'll share my use-case, maybe it could help. Consider the codes below
data class PersonItem(
val id : Int,
val isActive: Boolean = false
)
List Item
#Composable
fun PersonInfoItem() {
Column {
// First Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
// Last Name
TextField(
modifier = Modifier.onFocusEvent {
// report to view model this personItem received focus events
},
value = "",
onValueChange = {
}
)
}
VieModel
class ViewModel {
val currentFocusedItem : PersonItem? = null
fun onPersonItemFocused(personItem: PersonItem) {
// this is where I validate that if the personItem argument
// is not the same with the currentFocusedPerson, if they are not then the last one is not on focus anymore (set !isActive)
// otherwise if they are the same, we are still on the same PersonItem
}
Regardless of which TextField received or lost its focus, as long as I'm keeping track of the Id of the PersonItem being reported to the ViewModel I'm guaranteed that if its the same PersonItem, it will retain what ever state I put into it during its Focus events
(say, the item will change background color when either of the TextField received focus, and revert back to its original background when none of the TextField is focused).
Another thing to consider, these kinds components (i.e TextFields) report multiple inactive focus events during its initial composition, and it's kind of annoying.

how to assert that text not contains specific characters in android jetpack compose testing?

I'm trying to write some test cases for my compose functions.
I have an outlined Text field with a maximum value of 16 characters.
So I want to test this feature. Here is the test:
#Test
fun checkMaxTaxCodeLength_16Character() {
val taxCode = composeRule.onNodeWithTag(testTag = AUTHENTICATION_SCREEN_TAX_CODE_EDIT_TEXT)
for (i in 'A'..'Z')
taxCode.performTextInput(i.toString())
taxCode.assertTextEquals("ABCDEFGHIJKLMNOP")
}
But although I can see the input is correct, the test fails, and it seems assertTextEquals doesn't work correctly. So:
first of all, what am I doing wrong?
Second, is there any way to, instead of checking the equality, check the text does not contain specific characters?
here is the code of text field:
OutlinedTextField(
value = state.taxCode,
maxLines = 1,
onValueChange = { string ->
viewModel.onEvent(
AuthenticationEvent.TaxCodeChanged(string)
)
},
label = {
Text(text = stringResource(id = R.string.tax_code))
},
modifier = Modifier
.fillMaxWidth()
.testTag(TestingConstant.AUTHENTICATION_SCREEN_TAX_CODE_EDIT_TEXT)
)
The maximum length is handled in the view model. If the user adds more characters than 16, the view model won't update the state and keep the old value.
first of all, what am I doing wrong?
assertTextEquals() takes the value of Text and EditableText in your semantics node combines them and then does a check against the values you pass in. The order does not matter, just make sure to pass in the value of the Text as one of the arguments.
val mNode = composeTestRule.onNodeWithText("Email"))
mNode.performTextInput("test#mail.com")
mNode.assertTextEquals("Email", "test#mail.com")
Please note the text Email is the label for the textfield composable.
To get the semantic information about your nodes you can have
#Test
fun print_semantics_tree() {
composeTestRule.onRoot(useUnmergedTree = true).printToLog(TAG)
}
For the TAG you can use any string. After running the above test you can search the logcat with the specified TAG. You should see something like
|-Node #3 at (l=155.0, t=105.0, r=925.0, b=259.0)px
| Focused = 'false'
| ImeAction = 'Default'
| EditableText = 'test#mail.com'
| TextSelectionRange = 'TextRange(0, 0)'
| Text = '[Email]'
| Actions = [RequestFocus, GetTextLayoutResult, SetText, SetSelection,
OnClick, OnLongClick, PasteText]
Please note you can also obtain the semantics node object with an index operation rather than iterating through all the values.
val value = fetchSemanticsNode().config[EditableText]
assertEquals("test#mail.com", value.toString())
Ok, still, the problem is open, but I achieved what I wanted another way. I used semantic nodes to get what is in edit text and compared it with what it should be:
#Test
fun checkMaxTaxCodeLength_16Character() {
val taxCode = composeRule.onNodeWithTag(testTag = AUTHENTICATION_SCREEN_TAX_CODE_EDIT_TEXT)
for (i in 'A'..'Z')
taxCode.performTextInput(i.toString())
for ((key,value) in taxCode.fetchSemanticsNode().config)
if (key.name =="EditableText")
assertEquals("ABCDEFGHIJKLMNOP",value.toString())
}

reveal parts of text without causing a recomposition?

I'm basically writing a small quiz game with android jetpack compose in which you've got a question displayed and a text field below, I wanted to implement a "hint" (which appears under the text field and shows more and more of the correct answer for every bad answer). After implementing it, it causes a recomposition which selects a new question. Thus a question arrives, is there a way to update just the "hint" part of the screen or is that impossible ? (this might sound stupid but maybe I've missed something and there is a way)
Thanks in advance for every comment :)
You can recompose any part of a UI (without recomposing the entire screen or unrelated parts of the screen) as long as you isolate that section and only apply state changes to those parts that you want updated. The value you apply to your hint text should come from a viewmodel that contains a mutable state variable containing the text you want to update the hint with:
class MyViewModel : ViewModel() {
var hintText = mutableStateOf("")
fun onTextFieldChange(answer: String) {
// Retrieve your hint text from whatever api handles the user's response.
hintText.value = processAnswer(answer)
}
}
#Composable
fun Question() {
val vm = MyViewModel()
var text by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxWidth()) {
TextField(
value = text,
onValueChange = {
text = it
vm.onTextFieldChange(it)
},
label = { Text("Label") },
singleLine = true
)
HintText(vm = vm)
}
}
#Composable
fun HintText(
vm: MyViewModel
) {
Text(text = vm.hintText.value)
}

Compose TextField shows wierd behavior

I am trying to implement a TextField which inputs an amount and formats it as soon as it is typed and also limits it to 100,000.
#Composable
fun MainScreen(
viewModel: MyViewModel
) {
val uiState by viewModel.uiState.collectAsState()
Column {
AmountSection(
uiState.amount,
viewModel::updateAmount
)
Text(text = viewModel.logs)
}
}
#Composable
fun AmountSection(
amount: TextFieldValue,
updateAmount: (TextFieldValue) -> Unit
) {
BasicTextField(
value = amount,
onValueChange = updateAmount,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number
)
)
MyViewModel:
class MyViewModel: ViewModel() {
private val _uiState = MutableStateFlow(MyUiState())
val uiState: StateFlow<MyUiState> = _uiState
var logs by mutableStateOf("")
var text = ""
fun updateAmount(amount: TextFieldValue) {
val formattedAmount: String = amount.text.getFormattedAmount()
text += "input = ${amount.text}\n"
text += "output = $formattedAmount \n"
logs = text
_uiState.update {
it.copy(amount = TextFieldValue(formattedAmount, TextRange(formattedAmount.length))
}
}
}
data class MyUiState(val amount: TextFieldValue = TextFieldValue())
(logs and text are just for logging purpose. Was finding it difficult to share the logcat output so presented it this way)
Result:
When I press 6, the input is "12,3456" which is expected (ignore the currency)
My getFormattedAmount() function removes the last six as ( 123456 > 100000). It outputs "12,345" which is also correct. "12,345" is what gets displayed on the screen.
But when I press 7, I get the input "12,34567". Where did that 6 come from?? It was not in uiState.amount.
(Please ignore the last output line. getFormattedAmount only removes the last character if the amount exceeds the limit and it gave wrong output because it didn't expect that input)
I feel that I making some really silly mistake here and would be really thankful if somecome could help me find that out.
Edit based on the edit:-
From the looks of the question, it isn't much clear what you wish to achieve here, but this is my deduction -
You just want a TextField that allows numbers to be input, but only up to a maximum value (VALUE, not characters). When a digit-press by a user leads to the value exceeding your max value, you want that digit to be not entered of course, but you wish to reflect no changes at all in this case, i.e., the field value should remain intact.
Based on the above deduction, here is an example:-
First of all, f your uiState variable. I'm keeping it simple for the sake of clarity.
class VM: ViewModel(){
var fieldValue by mutableStateOf("")
fun onFieldUpdate(newValue){
if(newValue.toDouble() > 999999999999999)
return
else
fieldValue = newValue
}
}
#Composable
fun CrazyField(fieldValue: String, onFieldUpdate: (String) -> Unit){
TextField(value = fieldValue, onValueChange = onFieldUpdate)
}
Do not comment further without actually running this.
Original answer:-
Use a doubles parser.
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = {
if (it.toDouble() <= 100000)
text = it //Based on your use-case - it won't cut off text or limit the amount of characters
else text = it.subString(0,7) //In-case of pasting or fast typing
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
Found this comment on Compose slack channel by Sean.
As a model, you should assume that the keyboard may make aribitrary and large
edits each onValueChange. This may happen, for example, if the user uses
autocorrect, replaces a word with an emoji, or other smart editing features. To
correctly handle this, write any transformation logic with the assumption that
the current text passed to onValueChange is unrelated to the previous or next
values that will be passed to onValueChange.
So this is some issue with TextField & IME relationship.
I rewrote my getFormattedAmount function to format the given string without any assumptions (earlier it was assuming that amount is formatted till the last second character). Everything seems fixed now.

Jetpack Compose TextField blur event

I'm implementing form controls, with validations etc. I want an error message to be shown only when a user "blurs" the TextField, in other words, when the field loses its focus. In Angular, we have touched state that we can proceed from. How to listen for losing focus state in Jetpack Compose?
#Composable
fun Screen() {
TextField(
onBlur = {
// P.S. This parameter does not exist
}
)
}
You can use onFocusChanged.
Sample Code:
var color by remember { mutableStateOf(Black) }
Box(
Modifier
.border(2.dp, color)
// The onFocusChanged should be added BEFORE the focusable that is being observed.
.onFocusChanged { color = if (it.isFocused) Green else Black }
.focusable()
)
Update Answer:
TextField API update - merged onFocus and onBlur callbacks into a single onFocusChange(Boolean) callback with parameter
Source: Version 0.1.0-dev15 - July 22, 2020
If anyone is still having this issue, I solved this using #snorlax answer, but improving on it. My problem was a TextField data validation. The TextField is by nature focusable, different from a Box, so adding .focusable() after defining the event was not doing what I planned: the validation happened once the component appeared on screen, and obviously we didn't want that.
On the .onFocusChanged modifier, we can check the event types and use it to change the blurred state, triggering then the validation.
I had a validate() function and a validState boolean state that I used between my presenter and my view, but I'll make it simple here and you may ignore the implementation of the state class:
We have the focus events, and we can emulate a blur event with a simple flag:
class FieldValidState {
var value: Boolean by mutableStateOf(true)
}
#Composable
fun TextFieldValidation(validState: FieldValidState = remember { FieldValidState() })
Column {
var isBlurred = false
TextField(
Modifier
.onFocusChanged {
if (!it.isFocused && isBlurred) validState.value = validate()
if (it.isFocused && !isBlurred) isBlurred = true }
)
}
}
So what happens here is: I want the validation function to be triggered after lost focus on the TextField. On startup, the field is inactive (i.e. it.isFocused == false) but I don't want to validate then. So once the focus is on the field and it was not blurred before, isBlurred becomes true and when it loses focus, it will be validated.
Let me know if this helps! Thanks!

Categories

Resources