I have a Button composable that I need to get the text value when clicked.
Button(
onClick = {// Get the "TheText" from below },
) {
Text(
modifier = Modifier.padding(8.dp),
text = "TheText",
style = TextStyle(fontSize = 15.sp)
)
}
I am creating a type of quiz where the buttons text matches the correct answer.
I thin I may need to create a custom compposabe that takes the text as a parameter and also a callback function that will pass that text back up to my main program where I can Check for a correct answer.
I assume you want to input text instead of just displaying one.
#Composable
fun example() {
var text by remember { mutableStateOf("TheText") }
Column {
Button(
onClick = {
val useThisString = text
},
) { Text(text = text) } // Probably wanna put "Copy" here
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Optinal Label") }
)
}
}
Otherwise, if you just want the label of the button:
val buttonText = "TheText"
Button(
onClick = {
// Use variable here
},
) {
Text(
modifier = Modifier.padding(8.dp),
text = buttonText,
style = TextStyle(fontSize = 15.sp)
)
}
Ok here's how to store it in a variable
var retrievedValue by mutableStateOf("") //Assuming you must use it somewhere else as state
var currentValue by rememberSaveable { mutableStateOf("The Text") }
Button(
onClick = { retrievedValue = currentValue },
) {
Text(
modifier = Modifier.padding(8.dp),
text = currentValue,
style = TextStyle(fontSize = 15.sp)
)
}
Now you can use it anywhere you want. Changing the retrievedValue would trigger recompositions in any composable that reads it.
This should solve your problem. Use the retrievedValue as the value returned by a callback. In compose, for such stuff, you do not need callbacks. MutableState objects, whenever are modified, trigger a recomposition on the composables reading them.
Anyway, here when you click the button, you will get the value of the text, whatever it may be, you will always get the current value.
Related
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 = "")}) {
}
}
In a simple AlertDialog like the following
AlertDialog(
modifier = Modifier,
title = {
Text(text = "Title")
},
text = {
Column(
modifier = Modifier.fillMaxWidth()
) {
TextButton() {
Text("Text 1")
}
TextButton() {
Text("Text 2")
}
}
},
confirmButton = {},
dismissButton = {}
)
how can I set a spacing between title and the first TextButton?
I tried to set a .padding(top = X.dp) to the Column, or the first text button, but this seems to only create a space at the bottom of the AlertDialog.
Also setting a custom .height(X.dp) did not work.
I'm using Compose 1.0.3
As #Abhimanyu perfectly explains why it's not working right now, here's the workaround I'm using to achieve the desired padding: putting the title in the content. AlertDialog's title param is optional, so it can be omitted/set to null, and instead the actual title can be put in the text parameter (which holds the dialog content).
#Composable
fun MyComposable() {
AlertDialog(
title = null,
text = {
Column {
Text(
modifier = Modifier.padding(vertical = 16.dp),
text = "Actual title"
)
// Rest of the dialog content
}
}
)
}
This is NOT an answer. It only provides info on why this is not possible.
The requirement seems not achievable at this point (6th Oct 2021) with the current compose version (1.0.3).
Will update this once that is possible.
The AlertDialog code does not respect the padding values provided.
AlertDialog.kt
// Baseline distance from the first line of the text to the last line of the title
private val TextBaselineDistanceFromTitle = 36.sp
The text offset used for the positioning is calculated like this.
val textOffset = if (titlePlaceable == null) {
TextBaselineDistanceFromTop.roundToPx()
} else {
TextBaselineDistanceFromTitle.roundToPx()
}
The distance between the first text in the text composable and the last text in the title composable is always 36.sp.
The Alert Dialog code in compose seems too hackish currently and I could see a few TODO's in the code.
Hopefully, the code will be changed to handle more scenarios soon.
I'm using this composable as first child inside Column
#Composable
fun HackySpacer(space: Dp) {
Box(
modifier = Modifier
.height(space)
.fillMaxWidth()
) {
Text(text = "")
}
}
It's not perfect, but it works for my usecase.
Is now possible using the new AlertDialog from Compose Material 3.
The default spacing between title and text is much more reasonable and it is also possible to add Modifier.padding() or Spacer() to both.
implementation("androidx.compose.material3:material3:1.0.0-alpha01")
androidx.compose.material3.AlertDialog(
onDismissRequest = {
openDialog.value = false
},
title = {
Text(text = "Title", modifier = Modifier.padding(50.dp))
},
text = {
Spacer(Modifier.height(50.dp))
Text(text = "Turned on by default")
},
confirmButton = {
TextButton(
onClick = {
openDialog.value = false
}
) {
Text("Confirm")
}
},
dismissButton = {
TextButton(
onClick = {
openDialog.value = false
}
) {
Text("Dismiss")
}
}
)
I have a large number of texts in a row, and I would like to make every one of them change text decoration on press
(so the user can notice which text/tag is already selected)
(unselected: TextDecoration.None, selected: TextDecoration: Underlined)
(user can press selected text to unselect it)
var tagsSelected = mutableListOf<String>()
...
Text(text = "tech",
Modifier.clickable {
if (tagsSelected.contains("tech")) {
tagsSelected.remove("tech")
// RemoveTextDecoration ?
} else {
tagsSelected.add("tech")
// AddTextDecoration ?
}
}.padding(5.dp))
...
I've tried using variables (not a good idea cause it would require a lot of them), using an mutable array of boolean values (later observed as states) and none of that has brought results for me,
any amount of help will be appreciated,
thanks :)
You're creating a new mutableListOf on each recomposition. That's why new values are not getting saved. Check out how you should store state in compose.
rememberSaveable will save your state even after screen rotation(unlike remember), and mutableStateListOf is a variation of mutable list which will notify Compose about updates. I you need to save state even when you leave the screen and come back, check out about view models.
Also you can move your add/remove logic into extension so your code will look cleaner:
fun <E> MutableList<E>.addOrRemove(element: E) {
if (!add(element)) {
remove(element)
}
}
Final variant:
val tagsSelected = rememberSaveable { mutableStateListOf<String>() }
Text(
text = "tech",
modifier = Modifier
.clickable {
tagsSelected.addOrRemove("tech")
}
.padding(5.dp)
)
If you have many Text items which looks the same, you can repeat them using forEach:
val tagsSelected = rememberSaveable { mutableStateListOf<String>() }
val items = listOf(
"tech1",
"tech2",
"tech3"
)
items.forEach { item ->
Text(
text = item,
modifier = Modifier
.clickable {
tagsSelected.addOrRemove(item)
}
.padding(5.dp)
)
}
If you need to use selection state only to change text decoration, you can easily move it to an other composable and create a local variable:
#Composable
fun ClickableDecorationText(
text: String,
) {
var selected by rememberSaveable { mutableStateOf(false) }
Text(
text = text,
textDecoration = if(selected) TextDecoration.Underline else TextDecoration.None,
modifier = Modifier
.clickable {
selected = !selected
}
.padding(5.dp)
)
}
In this app, I have a screen where you can enter a title and content for a Note.
The screen has two composables DetailScreen() and DetailScreenContent.
Detailscreen has the scaffold and appbars and calls DetailScreenContents() which has two TextFields and a button.
I'm expecting the user to enter text in these fields and then press the button which will package the text into a NOTE object. My question is, how to pass the NOTE to the upper composable which is DETAILSCREEN() with a callback like=
onclick: -> Note or any other efficient way?
#Composable
fun DetailScreen(navCtl : NavController, mviewmodel: NoteViewModel){
Scaffold(bottomBar = { TidyBottomBar()},
topBar = { TidyAppBarnavIcon(
mtitle = "",
onBackPressed = {navCtl.popBackStack()},
)
}) {
DetailScreenContent()
}
}
#Composable
fun DetailScreenContent() {
val titleValue = remember { mutableStateOf("")}
val contentValue = remember { mutableStateOf("")}
val endnote by remember{ mutableStateOf(Note(
Title = titleValue.value,
Content = contentValue.value))}
Column(modifier = Modifier.fillMaxSize()) {
OutlinedTextField(value = titleValue.value,
onValueChange = {titleValue.value = it},
singleLine = true,
label = {Text("")}
,modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, end = 3.dp),
shape = cardShapes.small
)
OutlinedTextField(value = contentValue.value, onValueChange = {
contentValue.value = it
},
label = {Text("Content")}
,modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, end = 3.dp, top = 3.dp)
.height(200.dp),
shape = cardShapes.small,
)
Row(horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()){
Button(onClick = {
/**return the object to the upper composable**/
}, shape = cardShapes.small) {
Text(text = stringResource(R.string.Finish))
}
}
}
You could use state hoisting. Using lambdas is the most common way of hoisting state here.
Ok so here's DetailScreenContent(), say
fun DetailScreenContent(
processNote: (Note) -> Unit
){
Button( onClick = { processNote(/*Object to be "returned"*/) }
}
We are not literally returning anything, but we are hoisting the state up the hierarchy. Now, in DetailsScreen
fun DetailScreen(navCtl : NavController, mviewmodel: NoteViewModel){
Scaffold(bottomBar = { TidyBottomBar()},
topBar = { TidyAppBarnavIcon(
mtitle = "",
onBackPressed = {navCtl.popBackStack()},
)
}) {
DetailScreenContent(
processNote = {note -> //This is the passed object
/*Perform operations*/
}
)
//You could also extract the processNote as a variable, like so
/*
val processNote = (Note) {
Reference the note as "it" here
}
*/
}
}
This assumes that there is a type Note (something like a data class or so, the object of which type is being passed up, get it?)
That's how we hoist our state and hoist it up to the viewmodel. Remember, compose renders state based on variables here, making it crucial to preserve the variables, making sure they are not modified willy nilly and read from random places. There should be, at a time, only one instance of the variables, which should be modified as and when necessary, and should be read from a common place. This is where viewmodels are helpful. You store all the variables (state) inside the viewmodel, and hoist the reads and modifications to there. It must act as a single source of truth for the app.
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") }
)
}