TextField with Kotlin StateFlow - android

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") }
)
}

Related

onValueChange of BasicTextField is not triggered on setting value to TextFieldValue("") in Jetpack Compose

I want to execute some code when the value of BasicTextfield changes in Jetpack Compose.
Everything works fine in 2 conditions:
for any value change.
if all the textfield value is cleared using the device keyboard
But,
When I try to change the state value to empty text on click of a button, using this code :
textfieldstate.value = TextFIeldValue("")
onValueChange is not triggered.
Although if I set it to any other value, onValueChange is triggered.
textfieldstate.value = TextFIeldValue("FOO")
Code of Button/Icon click:
Icon(modifier = Modifier.clickable {
textfieldstate.value = TextFieldValue("")
}) {.....}
Is there a way to trigger onValueChange of BasicTextField when value of the field is cleared from an external button click event??
If you want to do it all at once as is more recommended, I would do this:
#Composable
fun AppContent(
viewModel: MyViewModel
) {
val state by viewModel.uiState.collectAsState()
MyPanel(
state = MyViewModel,
onValueChange = viewModel::onValueChange,
onClickButton = viewModel::onClickButton
)
}
#Composable
fun MyPanel(
state: MyTextFieldState,
onValueChange: (String) -> Unit,
onClickButton: () -> Unit
) {
TextField(
value = state.text,
onValueChange = onValueChange(it)
)
Button(
onClick = { onClickButton() }
) {
...
}
}
class MyViewModel: ViewModel() {
private val _uiState = MutableStateFlow(MyUiState())
val uiState = _uiState.asStateFlow()
fun onValueChange(str: String) {
_uiState.value = _uiState.value.copy(text = str)
}
fun onClickButton() {
_uiState.value = _uiState.value.copy(text = "")
}
}
data class MyUiState(
val text: String = ""
)
The code above mainly elevates the state of the TextField, processes all things in the viewModel, and wraps a layer of UI state with a data class. If there are other requirements, you can also add different parameters, for example, if there is an error in the TextField, it can be written as:
data class MyUiState(
val text: String = "",
val isTextError: Boolean = false
)
The onValueChange callback is useful to be informed about the latest state of the text input by users.
If you want to trigger some action when the state of (textFieldValue) changes, you can use a side effect like LaunchedEffect.
Something like:
var textFieldValue by remember() {
mutableStateOf(TextFieldValue("test" ))
}
LaunchedEffect(textFieldValue) {
//doSomething()
}
BasicTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
}
)
OutlinedButton(
onClick = { textFieldValue = textFieldValue.copy("") }
) {
Text(text = "Button")
}

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 = "")}) {
}
}

Mutable state of value from viewModel is not working

I have a mutablestate in my ViewModel that I'm trying to set and access in my composable. When I use delegated Property of remember it is working but after creating it in viewModel and accessing it in compose the state of the variable is empty how to use the state with viewModel
Everything is working fine when the state is inside the compose
var mSelectedText by remember { mutableStateOf("") }
But when i use it from viewModel change and set my OutlinedTextField value = mainCatTitle and onValueChange = {mainCatTitle = it} the selected title is not Showing up in the OutlinedTextField is empty
private val _mainCatTitle = mutableStateOf("")
val mainCatTitle: State<String> = _mainCatTitle
my Composable
var mSelectedText by remember { mutableStateOf("") }
var mainCatTitle = viewModel.mainCatTitle.value
Column(Modifier.padding(20.dp)) {
OutlinedTextField(
value = mainCatTitle,
onValueChange = { mainCatTitle = it },
modifier = Modifier
.fillMaxWidth()
.onGloballyPositioned { coordinates ->
mTextFieldSize = coordinates.size.toSize()
},
readOnly = true,
label = { Text(text = "Select MainCategory") },
trailingIcon = {
Icon(icon, "contentDescription",
Modifier.clickable { mExpanded = !mExpanded })
}
)
DropdownMenu(expanded = mExpanded,
onDismissRequest = { mExpanded = false },
modifier = Modifier.width(with(
LocalDensity.current) {
mTextFieldSize.width.toDp()
})) {
selectCategory.forEach {
DropdownMenuItem(onClick = {
mainCatTitle = it.category_name.toString()
mSelectedCategoryId = it.category_id.toString()
mExpanded = false
Log.i(TAG,"Before the CategoryName: $mainCatTitle " )
}) {
Text(text = it.category_name.toString())
}
}
}
}
Log.i(TAG,"Getting the CategoryName: $mainCatTitle " )
}
in my First log inside the DropDownMenuItem the log is showing the Selected field but second log is empty
Have attached the image
Your'e directly modifying the mainCatTitle variable from onClick, not the state hoisted by your ViewMoel
DropdownMenuItem(onClick = {
mainCatTitle = it.category_name.toString()
...
and because you didn't provide anything about your ViewModel, I would assume and suggest to create a function if you don't have one in your ViewModel that you can call like this,
DropdownMenuItem(onClick = {
viewModel.onSelectedItem(it) // <-- this function
...
}
and in your ViewModel you update the state like this
fun onSelectedItem(item: String) { // <-- this is the function
_mainCatTitle.value = item
}

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.

Jetpack Compose Number Input in to TextField

I am currently unable to capture user input in to a textfield when the KeyboardType of the keyboard is set to KeyboardType.Number.
If the keyboard is set to KeyboardType.Text, the Textfield updates as expected, however when set to KeyboardType.Number, the Textfield fails to update.
Why is this? and how can I change my code so that when the Textfield is clicked, a Number Keyboard is displayed ,and, when numbers are pressed, the relevant numbers are updated in the Textfield.
The following code DOES NOT update the textfield (When set to KeyboardType.Number)...
#Composable
fun MyNumberField() {
var text = remember { mutableStateOf("")}
val change : (String) -> Unit = { it ->
value.value = it
}
TextField(
value = text.value,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
onValueChange = change
)
}
The following code does update the textfield (When set to KeyboardType.Text)...
#Composable
fun MyNumberField() {
var text = remember { mutableStateOf("")}
val change : (String) -> Unit = { it ->
text.value = it
}
TextField(
value = value.value,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text),
onValueChange = change
)
}
Many Thanks
You are supposed to update text.value, not value.value there is a typo in your code change it to this.
#Composable
fun MyNumberField() {
var text = remember { mutableStateOf("")}
val change : (String) -> Unit = { it ->
value.value = it // you have this which is not correct and I don't think it even compiled
text.value = it // it is supposed to be this
}
TextField(
value = text.value,
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
onValueChange = change
)
}

Categories

Resources