dynamically change textDecoration on clickableText android compose - android

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

Related

How can I pass two lamdas in this situation to define my function in Kotlin

My question is simple and not as complicated as it might look like, so I have the function CoinListItem that has the following parameters, the first two parameters are two different Json domains that I need to use to display some items from them into the UI using Jetpack compose.
#Composable
fun CoinListItem (
coin: Coin,
coinDetail: CoinDetail, //
onItemClick: (Coin) -> Unit
) {
Row(
modifier = androidx.compose.ui.Modifier
.fillMaxWidth()
.clickable { onItemClick(coin) }
.padding(20.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "${coin.rank}. ${coin.name} (${coin.symbol})",
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis
)
Text(
text = if(coin.isActive) "active" else "inactive",
color = if(coin.isActive) Color.Green else Color.Red,
fontStyle = FontStyle.Italic,
textAlign = TextAlign.End,
style = MaterialTheme.typography.body2,
modifier = Modifier.align(CenterVertically)
)
Image(
painter = rememberAsyncImagePainter("${coinDetail.logo}"),
contentDescription = null,
modifier = Modifier.size(128.dp)
)
}
}
now in my CoinListScreen function below, and generally,
specifically at items(state.coins) { coin -> ,
I want to do something like, items(state.coins) { coin -> , coinDetail ->
but I'm not sure if it's even possible, or how is it possible if so. Otherwise, I get an error that It cannot resolve the reference coinDetail
#Composable
fun CoinListScreen(
navController: NavController,
viewModel: CoinListViewModel = hiltViewModel()
) {
val state = viewModel.state.value
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(state.coins) { coin -> **// I want to define coinDetail here as well**
CoinListItem(
coin = coin,
// coinDetail = coinDetail**,//this doesn't work ofcourse if coinDetail -> is not initialized**
onItemClick = {
navController.navigate(Screen.CoinDetailScreen.route + "/${coin.id}")
}
)
}
}
}
}
Without knowing the relation between Coin and CoinDetail it will be difficult to provide an exact answer, but here is something that will hopefully at least provide a nudge.
We need to associated each coin with the corresponding detail because Coin doesn't contain CoinDetail (it really probably should, but if it can't for some reason we will ignore this). One way to accomplish that could be something like this
state.coins.zipWith(state.coinDetails)
Downside, is this assumes that the two are both in lists of the same size and the order of the two matches (that is, that the details for coins[i] is coinDetails[i]). If that is not the case, then in order to make the relation, either the Coin or CoinDetail has an ID for the relating data (at least one needs it, both could have it).
If we have this ID we can do something like this
val coinDetailsMap Map<Coin, CoinDetails?> = coins.associateWith { coin ->
details.firstOrNull { detail ->
detail.id == coin.id
}
}
The above way will search through the entire list of details every time, if you want a slightly more performant approach this will only go through the details one time
val detailsMap: Map<String, CoinDetails> = details.associateBy(CoinDetails::id)
val coinDetailsMap Map<Coin, CoinDetails?> = coins.associateWith { coin ->
detailsMap[coin.detailsId]
}
Once you have some form of pairing between a Coin and a CoinDetails object you can pass Pair<Coin,CoinDetail> to the items function and get them via destructuring items(pairs) { (coin, details) -> /* ... */ }

Keep text in BasicTextField on back navigation

I have a BasicTextField in my jetpack compose function. When i click(user has input some text into the textfield by now) on a button to navigate to another composable in my NavHost, and from that new view click on back to the composable which i came from which has the textfield, the textfield is empty. I want to keep the text that the user typed in before navigating, but I can't figure it out how. Have looked here but found no answer.
Suggestions?
Here is my code:
#Composable
fun SearchBar(
modifier: Modifier = Modifier,
onSearch: (String) -> Unit = {}
) {
var text by remember {
mutableStateOf("")
}
Box(modifier = modifier) {
BasicTextField(
value = text,
onValueChange = {
text = it
onSearch(it)
},
maxLines = 1,
singleLine = true,
textStyle = TextStyle(color = Color.Black),
modifier = Modifier
.fillMaxWidth()
.shadow(5.dp, CircleShape)
.background(Color.White, CircleShape)
)
}
}
remember saves the value over recomposition (i.e., when your state changes, your composable automatically recomposes with the new state).
As per the Restore UI State guide, you can replace remember with rememberSaveable to save your state across:
Configuration changes
Process death and recreation
Your composable being put on the NavHost back stack
As well as any other case where your SearchBar could be removed from composition and then re-added (such as if you were using in Accompanist's Pager or in a LazyColumn or LazyRow).
var text by rememberSaveable {
mutableStateOf("")
}

Jetpack Compose - Access specific item in row/grid

How can I change the composables within a row?
For example if I had something like this:
#Composable
fun WordGrid() {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
MyCard("") basically a card with Text
MyCard("")
MyCard("")
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
MyCard("")
MyCard("")
MyCard("")
}
}
fun MyCard(text: String?) {
Card() {
Text(
text = text?: ""
)
}
}
and two buttons:
Button "A" and Button "B"
each time a button is clicked a card should get the text from the button, and then the next card and then the last.
clicking buttons A B A B would give you:
#Composable
fun WordGrid() {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
MyCard("A") basically a card with Text
MyCard("B")
MyCard("A")
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
MyCard("B")
MyCard("")
MyCard("")
}
}
How do you go about something like this unidirectionally? With XML you would be able to access the card.id directly from the ViewModel, here they have no id.
Is the only way to check if a button has been pressed and then create a state for that, pass this to the row and run through a for loop?
This seems much more complicated than having a simple id to grab.
In Compose you can't access views by ID. The only way is to manipulate the state which is used to build it.
I suggest you start with this youtube video which explains the basic principles of when you need to use state in compose. You can continue deepening your knowledge with state in Compose documentation.
In your case you can create a mutable state list of strings, build your views based on this list and update it using the buttons by index.
Here's a basic example of what you're trying to do:
val cards = remember { List(6) { "" }.toMutableStateList() }
var editingIndex by remember { mutableStateOf(0) }
Column {
Row {
val editButton = #Composable { text: String ->
Button({
cards[editingIndex] = text
editingIndex += 1
}) {
Text(text)
}
}
editButton("A")
editButton("B")
}
cards.chunked(cards.count() / 2).forEach { rowCards ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
rowCards.forEach {
MyCard(it)
}
}
}
}

Get Button Text

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.

Jetpack Compose: When using Modifier.selectable how do you prevent a layout composable from being selectable while scrolling?

In Jetpack Compose there is a Modifier extension called selectable.
Configure component to be selectable, usually as a part of a mutually exclusive group, where
only one item can be selected at any point in time.
I'm using this for a mutually exclusive radio group inside a scrollable list. In my case a LazyColumn. This works fine, clicking on the selectable areas lights them up and results in detected clicks. However I noticed that the area also lights up while "touching" these areas while scrolling.
I made a simple example composable if you want to see what I mean, simply scroll through the list and you will see how scrolling triggers a short selected state:
#Composable
fun Example() {
LazyColumn {
item {
repeat(100){
Column(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
.selectable(
selected = false,
onClick = { }
)
) {
Text("Example")
}
}
}
}
}
Has anyone figure out how to fix kind of behaviour? I tried looking for any related documentation at https://developer.android.com/jetpack/compose/gestures but nothing really explains how to "block" touch events while scrolling.
You can selectively enable Modifier.selectable(enabled) based on scroll state but even with derivedStateOf i see that there is huge performance loss.
val scrollState = rememberLazyListState()
val enableSelectable = derivedStateOf {
!scrollState.isScrollInProgress
}
Modifier
.fillMaxWidth()
.height(40.dp)
.selectable(
enabled = enableSelectable.value,
selected = false,
onClick = { }
)
I created a simple but longer example than you did, and included a video showing how it behaves with this code.
I believe what you are seeing is the ACTION_DOWN causing a ripple. It's not actually "selecting" the item because it does not change the selected state. I am not seeing the ripple when I scroll, but only when I keep my finger pressed on a specific row - the ripple disappears when my finger moves down.
I got the info about MotionEvents from this answer: https://stackoverflow.com/a/64594717/1703677
(Change the falses to true to see more info in the logs)
#Composable
fun Content() {
val selectedValue = remember { mutableStateOf("") }
LazyColumn {
item {
repeat(100) {
val label = "Item $it"
val selected = selectedValue.value == label
SingleRadioButtonWithLabel(label, selected) {
selectedValue.value = label
}
}
}
}
}
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun SingleRadioButtonWithLabel(
label: String,
selected: Boolean,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.selectable(
selected = selected,
onClick = {
onClick()
Log.e("TestApp", "Row onClick")
}
)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
Log.e("TestApp", "MotionEvent.ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e("TestApp", "MotionEvent.ACTION_MOVE")
}
MotionEvent.ACTION_UP -> {
Log.e("TestApp", "MotionEvent.ACTION_UP")
}
else -> false
}
false
}
) {
RadioButton(
selected = selected,
onClick = {
onClick()
Log.e("TestApp", "Radio Button onClick")
},
)
Text(
text = label,
modifier = Modifier.fillMaxWidth()
)
}
}

Categories

Resources