I am trying to strikethrough a text at the click of a button using Jetpack Compose but the UI is not updating.
I have tried to set a button and on the click of a button, deletedTasks gets updated and if my task is in deletedTasks then it should show up with a strikethrough.
This change only happens manually but I want it to be automatic
I am not sure how to go about this. This is my first time trying Jetpack Compose, any help would be appreciated
#Composable
fun toDoItem(title: String, category: String, task: Task) {
val rememberTask = remember { mutableStateOf(deletedTasks) }
Surface(...) {
Column (...) {
Row {
Column (...) {
if (!deletedTasks.contains(task)){
Text(text = title, style = MaterialTheme.typography.h4.copy(
fontWeight = FontWeight.Bold
))
} else {
Text(text = title, style = MaterialTheme.typography.h4.copy(
fontWeight = FontWeight.Bold
), textDecoration = TextDecoration.LineThrough)
}
}
IconButton(onClick = {
rememberTask.value.add(task)
}) {
Icon(imageVector = Icons.Filled.Check, contentDescription = "Check")
}
}
}
}
}
#Composable
fun recyclerView(tasks: MutableList<Task>) {
LazyColumn (modifier = Modifier.padding(vertical = 10.dp, horizontal = 80.dp)) {
items(items = tasks) {task ->
toDoItem(task.title, task.category, task)
}
}
}
You are using MutableList<>, which is not recommended for any collection use-cases in Jetpack Compose. I would advice changing it to a SnapshotStateList.
Your recyclerView composable would look like this
#Composable
fun recyclerView(tasks: SnapshotStateList<Task>) { ... }
and somewhere where you set things up (e.g ViewModel) would be like
tasks = mutableStateListOf<Task>( ... )
Using SnapshotStateList will guarantee an "update" or re-composition with any normal list operation you do to it such as,
list.add( <new task> )
list.remove( <selected task> )
list[index] = task.copy() <- idiomatic way of udpating a list
There is also SnapshotStateMap which is for Map key~value pair use-case.
But if you are curious as to how you would update your LazyColumn with an ordinary List, you would have to re-create the entire MutableList<Task> so that it will notify the composer that an update is made because the MutableList<Task> now refers to a new reference of collection of tasks.
Related
I have following composable
#Composable
fun ManageCredential(
manageCredentialViewModel: ManageCredentialViewModel
) {
val expandedList by manageCredentialViewModel.expandedList.collectAsState()
val text = "Hello"
Scaffold(
modifier = Modifier.padding(top = 100.dp),
content = { padding ->
Image(
painter = if (expandedList.contains(text)) painterResource(id = R.drawable.drop_down_expand) else painterResource(
id = R.drawable.drop_down_collapse
),
contentDescription = text, modifier = Modifier.padding(padding).clickable {
if (expandedList.contains(text)) {
manageCredentialViewModel.removeFromList(text)
} else {
manageCredentialViewModel.addToList(text)
}
}
)
})
}
The view model is as follows
#HiltViewModel
class ManageCredentialViewModel #Inject constructor(
) : ViewModel() {
private val _expandedList = MutableStateFlow<MutableList<String>>(mutableListOf())
val expandedList = _expandedList.asStateFlow()
fun addToList(value: String) {
_expandedList.value.add(value)
}
fun removeFromList(value: String) {
_expandedList.value.remove(value)
}
}
I just want to toggle the image if a particular text is added to the list but it never happens
For recomposition to be triggered you need to change value of State. What you currently do is adding or removing items from existing list.
You can create SnapshotStateList with mutableStateListOf. Any changes in this list by removing, adding or updating an existing item with a new item, easily can be done with data class copy function, you can trigger recomposition and good thing about it is it only triggers recomposition for the Composable that reads that item such as LazyColumn item.
You can refer this answer also
Jetpack Compose lazy column all items recomposes when a single item update
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) -> /* ... */ }
I'm struggling to use Jetpack Compose Animation to achieve a (supposedly simple) effect:
in the case of an error, a control's background color should flash red, and after a short delay then fade back to normal (transparent).
My current approach is to model this with a boolean state shouldFlash which is set to true when an error occurs, and is set back to false when the animation completes. Unfortunately it seems the finishedListener passed to animateColorAsState is never called. I attached a debugger and also added a log statement to verify this.
What am I doing wrong?
Sample (button triggers error):
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
FlashingText(flashing = shouldFlash, text = text) {
shouldFlash = false
text = "Animation done"
}
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
#Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
if (flashing) {
val flashColor by animateColorAsState(
targetValue = Color.Red,
finishedListener = { _ -> flashFinished() }
)
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
} else {
Text(text = text)
}
}
Compose version: 1.0.5 (latest stable at time of writing), also in 1.1.0-beta02
Anyway, to understand this, you need to know how the animateColorAsState works internally.
It relies on recompositions - is the core idea.
Every time a color changes, a recomposition is triggered, which results in the updated color value being reflected on the screen. Now, what you are doing is just using conditional statements to DISPLAY DIFFERENT COMPOSABLES. Now, one Composable is actually referring to the animating value, that is, the one inside your if block (when flashing is true). On the other hand, the else block Composable is just a regular text which does not reference it. That is why you need to remove the conditional. Anyway, because after removing the conditional, what remains is only a single text, I thought it would be a waste to create a whole new Composable out of it, which is why I removed that Composable altogether and pasted the Text inside your main Composable. It helps to keep things simpler enough. Other than this, the answer by #Rafiul does work, but there is not really a need for a Composable like that, so I would still recommend using this answer instead, so that the code is easier to read.
ORIGINAL ANSWER:
Try moving the animator outside the Child Composable
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
val flashFinished: (Color) -> Unit = {
shouldFlash = false
text = "Animation done"
}
val flashColor by animateColorAsState(
targetValue = if (shouldFlash) Color.Red else Color.White,
finishedListener = flashFinished
)
//FlashingText(flashing = shouldFlash, text = text) -> You don't need this
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
Change your code like this.
FlashingBackground
#Composable
fun FlashingBackground() {
Column(modifier = Modifier.size(200.dp)) {
var shouldFlash by remember { mutableStateOf(false) }
var text by remember { mutableStateOf("Initial") }
FlashingText(flashing = shouldFlash, text = text) {
shouldFlash = false
text = "Animation done"
}
Button(onClick = {
shouldFlash = true
}) {
Text(text = "Flash")
}
}
}
FlashingText
#Composable
fun FlashingText(flashing: Boolean, text: String, flashFinished: () -> Unit) {
val flashColor by animateColorAsState(
targetValue = if(flashing) Color.Red else Color.White,
finishedListener = { _ -> flashFinished() }
)
Text(text = text, color = Color.White, modifier = Modifier.background(flashColor))
}
Edited:
The problem with your code is you are initializing animateColorAsState when you are clicking the Flash button and making shouldFlash = true. So for the first time, it just initializes the animateColorAsState, doesn't run the animation. So there will be no finishedListener call as well. Since finishedListener isn't executed, shouldFlash stays to true. So from the next call shouldFlash is already true there will be no state change. That's why from the subsequent button click, it doesn't recompose the FlashingText anymore. You can put some log in your method you won't see FlashingText after the first button click.
Keep in mind: targetValue = Color.Red will not do anything. target value should be either a state or like this condition if(flashing) Color.Red because you need to change the state to start the animation.
#Phillip's answer is also right. But I don't see any extra advantage in moving the animator outside the Child Composable if you use it like the above.
I'm trying to iterate over a List of objects, for each of the object I want to display a composable card. The problem is that you cant call Composable functions from inside the list.forEach{} brackets.
The Code:
#Composable
fun Greeting(listy : List<SomethingForLater>) {
LazyColumn {
listy.forEach {
//error here
testCard(somethingForLater = it)
}
}
}
#Composable
fun testCard(somethingForLater: SomethingForLater){
val theme = MaterialTheme
Card(shape = theme.shapes.small,backgroundColor = theme.colors.secondary){
Column {
Row {
Text(
text = somethingForLater.task,
modifier = Modifier.padding(start = 5.dp,
top = 3.dp,bottom = 3.dp
),
fontSize = 18.sp,
fontWeight = FontWeight.Bold,
)
}
}
}
}
There is items parameter in LazyColumn
LazyColumn {
items(listy) { message ->
testCard(message)
}
}
Or you can simply change LazyColumn to Column
LazyColumn does not provide any composable content. So you have to wrap your composable functions inside items parameter.
#Composable
fun Greetings(listy : List<SomethingForLater>) {
LazyColumn {
items(listy.size) {
listy.forEach { somethingForLater ->
TestCard(somethingForLater = somethingForLater)
}
}
}
}
Let's suppose I'm using some library that's intended to provide some UI Widgets.
Let's say this library provides a Button Widget called FancyButton.
In the other hand, I have a new project created with Android Studio 4 that allows me to create a new project with an Empty Compose Activity.
The question is:
How should I add this FancyButton to the view stack? Is it possible? Or with Jetpack Compose I can only use components that had been developed specifically for Jetpack Compose. In this case, AFAIK I could only use Android standars components (Text, MaterialTheme, etc).
If I try to use something like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Greeting("Android")
FancyButton(context, "Some text")
}
}
}
then I get this error:
e: Supertypes of the following classes cannot be resolved. Please make sure you have the required dependencies in the classpath.
Currently (as of 0.1.0-dev04), there is not a good solution to this. In the future, you'll be able to simply call it as FancyButton("Some text") (no context needed).
You can see a demo of what it will look like in the Compose code here.
Update in alpha 06
It is possible to import Android View instances in a composable.
Use ContextAmbient.current as the context parameter for the View.
Column(modifier = Modifier.padding(16.dp)) {
// CustomView using Object
MyCustomView(context = ContextAmbient.current)
// If the state updates
AndroidView(viewBlock = ::CustomView, modifier = modifier) { customView ->
// Modify the custom view
}
// Using xml resource
AndroidView(resId = R.layout.view_demo)
}
You can wrap your custom view within the AndroidView composable:
#Composable
fun RegularTextView() {
AndroidView(
factory = { context ->
TextView(context).apply {
text = "RegularTextView"
textSize = 34.dp.value
}
},
)
}
And here is how to update your custom view during a recomposition, by using the update parameter:
#Composable
fun RegularTextView() {
var string by remember {
mutableStateOf("RegularTextView")
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AndroidView(
factory = { context ->
TextView(context).apply {
textSize = 34.dp.value
}
},
update = { textView ->
textView.text = string
}
)
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
string = "Button clicked"
},
) {
Text(text = "Update text")
}
}
}
#Composable
fun ButtonType1(text: String, onClick: () -> Unit)
{
Button (
modifier=Modifier.fillMaxWidth().height(50.dp),
onClick = onClick,
shape = RoundedCornerShape(5.dp),
border = BorderStroke(3.dp, colorResource(id = R.color.colorPrimaryDark)),
colors = ButtonDefaults.buttonColors(contentColor = Color.White, backgroundColor = colorResource(id = R.color.colorPrimaryDark))
)
{
Text(text = text , color = colorResource(id = R.color.white),
fontFamily = montserrat,
fontWeight = FontWeight.Normal,
fontSize = 15.sp
)
}
}