I have an OutlineTextField where I am editing and updating it by calling an API on click of Save button.
Now what I want if the user doesn't change the text, api call should not happen and then onclick of save , there should not be any api call and it should got to the previous screen.
Below is my code snippet:
OutlinedTextField(
value = value,
modifier = modifier,
onValueChange = onValueChange,
placeholder = PlaceholderComponent
)
I solved it by checking initial viewmodel text.
If I've understood the question clearly, as #Gabriele Mariotti suggested, you can store the previous value of the text field and then compare it with the actual in the text field itself on the button click.
You can arrange the code with a Composable function like so:
#Composable
fun SaveButton(
modifier: Modifier = Modifier
) {
var currentValue by remember { mutableStateOf("") }
var previousValue by remember { mutableStateOf(currentValue) }
val context = LocalContext.current
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxSize()
) {
OutlinedTextField(
value = currentValue,
modifier = modifier,
onValueChange = {
currentValue = it
}
)
Button(onClick = {
if (currentValue != previousValue) {
Toast.makeText(context, "API request started", Toast.LENGTH_SHORT).show()
previousValue = currentValue
// Handle API request
} else {
Toast.makeText(context, "The text has not changed. Returning to the previous screen...", Toast.LENGTH_LONG).show()
// Handle on back screen
}
}) {
Text(text = "Save")
}
}
}
Related
so probably dumb question for someone with experience with Jetpack Compose, but for some reason my Toast are not displaying. Here's the snippet for it. I'm sending 3 toasts with .show() in different parts of the onCreate method, and still nothing happens. I also tried using LocalContext.current as context inside a #Composable function but same result, nothing is displayed. I see online many different examples, even videos on youtube in which this exact same could should run. Anybody knows why?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Toast.makeText(this, " ASDASDASD ", Toast.LENGTH_LONG).show()
setContent {
Toast.makeText(this, " ASDASDASD ", Toast.LENGTH_LONG).show()
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Toast.makeText(this, " ASDASDASD ", Toast.LENGTH_LONG).show()
Greeting("Android")
}
}
}
}
A sample code to show Toast.
setContent {
val context = LocalContext.current
MyAppTheme {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
) {
TextButton(
onClick = {
Toast.makeText(context, "Toast", Toast.LENGTH_LONG).show()
},
) {
Text(text = "Show Toast")
}
}
}
}
Let me break down the important things to look into in this code.
Toast should not be part of the composable code, rather it should be part of the side effect code.
(e.g. onClick(), LaunchedEffect, DisposableEffect, etc.)
Get the context inside the composable code using LocalContext.current. Note that you have to store it in a variable as the code inside onClick() does not have composable scope. (As mentioned above).
Composables recompose very frequently depending on the UI changes, we don't want to show a Toast every time the composable recomposes.
Refer to Side Effects Docs to understand it better.
You need to use a scaffold for displaying toasts
Code:
class SimpleFormActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val scaffoldState = rememberScaffoldState()
var inputFieldState by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) { padding ->
Column(
Modifier
.fillMaxSize()
.padding(50.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
TextField(
value = inputFieldState,
label = {
Text(text = "Enter your name")
},
onValueChange = {
inputFieldState = it
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(20.dp))
Button(
onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(
inputFieldState,
duration = SnackbarDuration.Short
)
}
}) {
Text(text = "Click")
}
}
}
}
}
}
Demo:
This his how my composable looks like
#Composable
fun MyComposable(
email:String?,
onEmailChanged:(email)-> Unit,
buttonClicked:()->Unit,
validEmail:Boolean
){
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester = focusRequester),
value = email ?: "",
status = someStatus // Default, Error, Success
onValueChange = { email -> onEmailChanged(email)},
)
Button(
onClick = {
focusManager.clearFocus()
buttonClicked()
if(!validEmail) focusRequester.requestFocus()
},
) {
Text(text = "Button")
}
}
}
I have added the basic code for explanation, i am facing issue with focusRequester when a button is clicked.
For accessibility reasons once the button is clicked, and if the email is invalid, i would like the focus to go back to TextField so that accessibility can announce it's state and label.
I tried clearing the focus and then requesting it again, but it only works for the first time. And i would like the TextField to gain focus every-time a button is clicked and email is invalid.
Am i missing something here?
try this it works totally fine.
#Composable
fun Test() {
val context = LocalContext.current
val emailState = remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
Column {
TextField(
modifier = Modifier
.focusRequester(focusRequester)
.fillMaxWidth(),
value = emailState.value,
onValueChange = {
emailState.value = it
},
)
Button(onClick = {
focusManager.clearFocus()
if (emailState.value.isEmpty() ) {
focusRequester.requestFocus()
Toast.makeText(context, "error", Toast.LENGTH_SHORT).show()
}
}) { Text(text = "Button")
}
}
}
I couldn't find any way to get the focus back on TextField if it was already focused, once the button was clicked.
I ended up using live-region for announcement.
TextField(
modifier = Modifier
.fillMaxWidth()
.semantics {
liveRegion = LiveRegionMode.Assertive
}),
value = email ?: "",
status = someStatus // Default, Error, Success
onValueChange = { email -> onEmailChanged(email)},
)
Button(
onClick = {
focusManager.clearFocus()
buttonClicked()
},
) {
Text(text = "Button")
}
}
Starting to Learn JetPack Compose. I'm struggling now with State hosting. I have this simple example to press a Button to show in a Text component the content of an array. I'm able to make it work if the variable is inside my #Composable. When applying State Hoisting (taking the variable for my composable) I'm finding some issues.
This is the code that is working fine
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
NextValue(listadoNumeros)
#Composable
fun NextValue(listado: List<Int>) {
var position by rememberSaveable {mutableStateOf (0)}
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
){
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
This is the code that is not working correctly
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by rememberSaveable {mutableStateOf (0)}
NextValue(position = n,listadoNumeros)
#Composable
fun NextValue(position:Int,listado: List<Int>) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
Error message, as you can expect, is "position can't be reassigned". I see why, but don't know how to fix it. I read about onValueChanged in TextField, etc, but don't know if it's applicable here.
position += 1 is equivalent to position = position + 1. In Kotlin, function arguments are val and cannot be reassigned within the scope of the function. That is why the compiler will complain and prevent you from doing that.
What you want to do is to add an extra event callback within the function and perform this addition at the function call site.
#Composable
fun NextValue(position: Int, listado: List<Int>, onPositionChange: (Int) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { onPositionChange(position + 1) }) {
Text(text = "Next")
}
}
}
You can use the above composable as
val listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by remember { mutableStateOf(0) }
NextValue(position = n, listadoNumeros, onPositionChange = { newPosition -> n = newPosition})
Whenever the user clicks on the button, an event is sent back up to the caller and the caller decides what to do with the updated information. In this case, recreate the composable with an updated value.
I am showing a list of rows with one word in it, inside a LazyColumn. On clicking the row, an edit form opens. The data is coming from a room database.
Since the row is on a separate composable function, I can open many different edit forms together (one in each row). But I want to show only one edit form in the whole list at a time. If I click one row to open an edit form, the rest of the open forms on the other rows should be closed. How can I do that?
Here is the code:
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
var newWord by remember { mutableStateOf("") }
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(words) { word ->
WordItemLayout(
word = word,
onSaveUpdatedWord = { onUpdateWord(it) },
onTrashClicked = { onDeleteWord(it) }
)
}
}
#Composable
fun WordItemLayout(word: Word, onSaveUpdatedWord: (Word) -> Unit, onTrashClicked: (Word) -> Unit) {
var showEditForm by remember { mutableStateOf(false) }
var editedWord by remember { mutableStateOf(word.word) }
val context = LocalContext.current
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primaryVariant)
.padding(vertical = 12.dp, horizontal = 24.dp)
.clickable {
showEditForm = !showEditForm
editedWord = word.word
},
verticalAlignment = Alignment.CenterVertically,
) {
Image(painter = painterResource(R.drawable.ic_star), contentDescription = null)
Text(
text = word.word,
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.padding(start = 16.dp)
.weight(1f)
)
// Delete Button
IconButton(
onClick = {
showEditForm = false
onTrashClicked(word)
Toast.makeText(context, "Word deleted", Toast.LENGTH_SHORT).show()
},
modifier = Modifier.size(12.dp)
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Delete Word",
tint = Color.White
)
}
}
// word edit form
if (showEditForm) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Bottom
) {
TextField(
value = editedWord,
onValueChange = { editedWord = it },
modifier = Modifier.weight(1f),
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White) // TextField Background Color
)
// Update Button
Button(
onClick = {
val updatedWord: Word = word
if (updatedWord.word != editedWord.trim()) {
updatedWord.word = editedWord.trim()
onSaveUpdatedWord(updatedWord)
Toast.makeText(context, "Word updated", Toast.LENGTH_SHORT).show()
}
showEditForm = false
},
modifier = Modifier.padding(start = 8.dp)
) {
Icon(imageVector = Icons.Filled.Done, contentDescription = "Update Word")
}
}
}
}
}
Thanks for your help!
An approach: In your view model, declare an openRowIndex state (this will store the index of the opened row, you can initialize it to -1 for example).
Define a method that can change this state, for example updateOpenRowIndex
I'm not sure what kind of state holder you are using in your view model. I will use StateFlow for this answer. In your view model declare the new state and method:
private val _openRowIndex = MutableStateFlow(-1)
val openRowIndex: StateFlow<Int> = _openRowIndex
fun updateOpenRowIndex(updatedIndex: Int) {
_openRowIndex.value = updatedIndex
}
For each row compisable, pass in the index of it inside the LazyColumn. You can get the indices using the itemsIndexed method. Also collect your openRowIndex, and pass that to the composable as well. Pass in also the method that updates the open row index:
itemsIndexed(words) { index, word ->
//get the current opened row state and collect it (might look different for you if you are not using StateFlow):
val openRowIndex = wordViewModel.openRowIndex.collectAsState()
WordItemLayout(
word = word,
onSaveUpdatedWord = { onUpdateWord(it) },
onTrashClicked = { onDeleteWord(it) },
index = index, //new parameter!
openRowIndex = openRowIndex.value //new parameter!
onUpdateOpenedRow = wordViewModel::updateOpenRowIndex //new parameter!
)
}
Now, in the row composable, simply check if the index and openRowIndex match, and display an opened row only if they match. Now to update the open row: make the Row clickable, and on click use view models updateOpenRowIndex method to update state to index. Compose will handle the rest and recompose when the state changes with the newly opened row!
fun WordItemLayout(
word: Word,
onSaveUpdatedWord: (Word) -> Unit,
onTrashClicked: (Word), -> Unit,
index: Int, //new parameters
openRowIndex: Int,
onUpdateOpenedRow: (Int) -> Unit
) {
if(index == openRowIndex) {
//display this row as opened
} else {
//display this row as closed
}
}
As I said, make the row clickable and call the update function:
Row(
modifier = Modifier.clickable {
onUpdateOpenedRow(index)
//additional instructions for what to happen when row is clicked...
}
//additional row parameters...
)
I have textField composable and button composable. I want that clicking at the button would erase the text in the textField composable.
example:
var text by remember
mutableStateOf(TextFieldValue(""))}
TextField(
value = text,
onValueChange = { newValue -> text = newValue },
modifier = Modifier
.padding(8.dp),
)
Button(
onClick = {
//TODO: clean the text in textFiled
},
modifier = Modifier
.size(200.dp, 40.dp)
) {
Text(text = "erase textField"
}
thanks
You can just simply reset the value of the text mutableState:
Button(onClick = { text = TextFieldValue("") })
Create a mutableState as follows -> var textState by remember { mutableStateOf("") }
Create Textfield -> TextField(value = textState, onValueChange = { textState = it })
In the onClick of the button invoke the textState -> textState = ""