I am trying to show a list of installed apps and a checkbox next to each app. The scrolling is terrible, it's noticably laggy. It looks like i am doing something wrong but i can't figure it out. how can I improve it?
#Composable
fun AppList(infoList: MutableList<android.content.pm.ResolveInfo>) {
val ctx = LocalContext.current
LazyColumn {
items(infoList) { info ->
var isChecked by remember { mutableStateOf(false) }
var icon by remember { mutableStateOf(info.loadIcon(ctx.packageManager)) }
var label by remember { mutableStateOf( info.loadLabel(ctx.packageManager).toString()) }
Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()
) {
Image(
painter = rememberImagePainterMine(icon),
contentDescription = label,
contentScale = ContentScale.Fit,
modifier = Modifier.padding(16.dp)
)
// Display the app name
Text(
text =label,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.weight(1f)
)
// Display the checkbox on the right
Box(modifier = Modifier.clickable { isChecked = !isChecked }) {
Checkbox(
checked = isChecked,
onCheckedChange = { isChecked = it },
modifier = Modifier.padding(16.dp)
)
}
}
}
}
}
#Composable
fun rememberImagePainterMine(drawable: Drawable): Painter = remember(drawable) {
object : Painter() {
override val intrinsicSize: Size
get() = Size(drawable.intrinsicWidth.toFloat(), drawable.intrinsicHeight.toFloat())
override fun DrawScope.onDraw() {
drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
drawable.draw(this.drawContext.canvas.nativeCanvas)
}
}
}
Are you running your app in release mode? It makes a big difference as to how smooth Jetpack Compose will be (due to the amount of debug code under the hood).
Google themselves explain it as follows:
If you're finding performance issues, make sure to try running your app in release mode. Debug mode is useful for spotting lots of problems, but it imposes a significant performance cost, and can make it hard to spot other code issues that might be hurting performance. You should also use the R8 compiler to remove unnecessary code from your app. By default, building in release mode automatically uses the R8 compiler.
Additionally, you should consider using keys with your LazyColumn to help Compose figure out if your data moves around or changes completely.
Without your help, Compose doesn't realize that unchanged items are just being moved in the list. Instead, Compose thinks the old "item 2" was deleted and a new one was created, and so on for item 3, item 4, and all the way down. The result is that Compose recomposes every item on the list, even though only one of them actually changed.
The solution here is to provide item keys. Providing a stable key for each item lets Compose avoid unnecessary recompositions. In this case, Compose can see that the item now at spot 3 is the same item that used to be at spot 2. Since none of the data for that item has changed, Compose doesn't have to recompose it.
Related
So I'm trying to do a simple app that changes the color to red or green and goes back to black if a price fluctuates, my currently implementation is this
#Composable
fun LaunchingComposable() {
var coinPrice by remember {
mutableStateOf(2000.30)
}
CoinHeader(modifier = Modifier.padding(horizontal = 8.dp),
"http://myicon.com/image.png",
coinPrice
)
LaunchedEffect(Unit) {
delay(3000)
coinPrice = 2000.40
delay(3000)
coinPrice = 2000.20
delay(3000)
coinPrice = 2000.10
delay(3000)
coinPrice = 2000.20
}
}
...
#Composable
fun CoinHeader(modifier: Modifier, coinImageUrl: String, currentPrice: Double) {
val baseColor = remember { Animatable(Black) }
val previousPrice = remember {
currentPrice
}
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
modifier = Modifier
.width(30.dp)
.height(30.dp)
.padding(end = 4.dp)
.data(coinImageUrl)
.build()
)
LaunchedEffect(currentPrice) {
if (previousPrice < currentPrice) {
baseColor.animateTo(Red, animationSpec = tween(1000))
} else {
baseColor.animateTo(Green, animationSpec = tween(1000))
}
baseColor.animateTo(Black, animationSpec = tween(1000))
}
Text(text = currentPrice.toPrice(), color = baseColor.targetValue)
}
}
In my last composable I'm expecting the values to change to Green - Red - Red - Green
I need to always store previous value of my coinPrice in order to compare it, do a fade animation with the color and then come back to the black color.
Currently this is my output
The problems are 2
Fade in - out color from red to black or green to black not happening
Seems like it always compare with the first value
Can anyone explain to me why if I recompose after coinPrice has been changed, the value of previous is not set correctly ?
Your current implementation of previousPrice is really just original price because it is never changed. You never set it to a new value, so it forever holds the first currentPrice ever received. Your remember call doesn't even have a key, so it will never recompute it, but even if you did, there would be no way to compute it to be the previous value instead of the current one.
I think you will have to use a mutable wrapper class around the remembered previous price so you can actually change it. An array may suffice, or you could write a specific data class to wrap a var.
Something like this:
val rememberedPreviousPrice = remember { arrayOf(currentPrice) }
val previousPrice = rememberedPreviousPrice[0]
rememberedPreviousPrice[0] = currentPrice
Secondly, you're using baseColor.targetValue instead of baseColor.value, so it's not using the animated value, but jumping right to the final ("target") color.
There could be other problems in your code. I'm not sure because I haven't done much with LaunchedEffects or animations in Compose yet myself.
By the way, you should not use Double for currency. Use BigDecimal instead. Read here and here.
I was trying to create a sample Tab View in Jetpack compose, so the structure will be like
Inside a Parent TabRow we are iterating the tab title and create Tab composable.
More precise code will be like this.
#OptIn(ExperimentalPagerApi::class)
#Composable
private fun MainApp() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
backgroundColor = MaterialTheme.colors.surface
)
},
modifier = Modifier.fillMaxSize()
) { padding ->
Column(Modifier.fillMaxSize().padding(padding)) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
val tabContents = listOf(
"Home" to Icons.Filled.Home,
"Search" to Icons.Filled.Search,
"Settings" to Icons.Filled.Settings
)
HorizontalPager(
count = tabContents.size,
state = pagerState,
contentPadding = PaddingValues(horizontal = 32.dp),
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { page ->
PagerSampleItem(
page = page
)
}
TabRow(
selectedTabIndex = pagerState.currentPage,
backgroundColor = MaterialTheme.colors.surface,
contentColor = MaterialTheme.colors.onSurface,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.pagerTabIndicatorOffset(pagerState, tabPositions)
.height(4.dp)
.background(
color = Color.Green,
shape = RectangleShape
)
)
}
) {
tabContents.forEachIndexed { index, pair: Pair<String, ImageVector> ->
Tab(
selected = pagerState.currentPage == index,
selectedContentColor = Color.Green,
unselectedContentColor = Color.Gray,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(text = pair.first) },
icon = { Icon(imageVector = pair.second, contentDescription = null) }
)
}
}
}
}
}
#Composable
internal fun PagerSampleItem(
page: Int
) {
// Displays the page index
Text(
text = page.toString(),
modifier = Modifier
.padding(16.dp)
.background(MaterialTheme.colors.surface, RoundedCornerShape(4.dp))
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
.padding(8.dp)
.wrapContentSize(Alignment.Center)
)
}
And coming to my question is whenever we click on the tab item, the inner content get recompose so weirdly. Im not able to understand why it is happens.
Am attaching an image of the recomposition counts below, please take a look that too, it would be good if you guys can help me more for understand this, also for future developers.
There are two question we have to resolve in this stage
Whether it will create any performance issue, when the view getting more complex
How to resolve this recompostion issue
Thanks alot.
… whenever we click on the tab item, the
inner content get recompose so weirdly. Im not able to understand why
it is happens...
It's hard to determine what this "weirdness" is, there could be something inside the composable your'e mentioning here.
You also didn't specify what the API is, so I copied and pasted your code and integrated accompanist view pager, then I was able to run it though not on an Android Studio with a re-composition count feature.
And since your'e only concerned about the Text and the Icon parameter of the API, I think that's something out of your control. I suspect the reason why your'e getting those number of re-composition count is because your'e animating the page switching.
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
Though 'm not able to try this on another Android Studio version with the re-composition feature, I think (though I'm not sure) scrolling to another page without animation will yield less re-composition count.
coroutineScope.launch {
pagerState.scrollToPage(index)
}
If it still bothers you, the best course of action is to ask them directly, though personally I wouldn't concerned much about this as they are part of an accepted API and its just Text and Icon being re-composed many times by an animation which is also fine IMO.
Now if you have some concerns about your PagerSampleItem stability(which you have a full control), based on the provided code and screenshot, I think your'e fine.
There's actually a feature suggested from this article to check the stability of a composable, I run it and I got this report.
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun PagerSampleItem(
stable page: Int
)
Everything about this report is within the article I linked.
Also, your Text and Icon are using String and ImageVector which is stable and immutable (marked by #Immutable) respectively.
So TLDR, IMO your code is fine, your PagerSampleItem is not re-composing in the screenshot.
I saw that a new parameter has recently been added to the Material3 Top App Bar Composables on Jetpack Compose:
fun CenterAlignedTopAppBar(
...
scrollBehavior: TopAppBarScrollBehavior? = null
) {}
What I understood from the documentation is that this should enable us to implement the behaviour that the app bar hides at scrolling the content. However, I did not manage to implement this, as the only example I found on StackOverflow seems to no longer work on the latest version of Jetpack Compose and is giving the error No value passed for parameter 'state'.
Can anybody provide an example? What I want to achieve is a Scaffold, where as topBar a CenterAlignedTopAppBar is provided, that scrolls out on top of the screen if the scrollable content of the Scaffold is scrolled.
Thanks a lot for your help.
I finally managed to get it to work:
val topAppBarScrollState: TopAppBarScrollState = rememberTopAppBarScrollState()
val scrollBehavior = remember { TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarScrollState) }
CenterAlignedTopAppBar(
modifier = modifier,
title = { Text(text = stringResource(id = titleResource)) },
actions = {
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.Filled.Settings,
contentDescription = null,
)
}
},
scrollBehavior = scrollBehavior
)
This only seems to be necessary since Compose version 1.2.0-rc02 though, as on older versions the solution in the post linked in my answer still works.
I'm trying to create a clickable card that changed its color after it is clicked.
I have (code relevant to the question is documented as "This is relevant to the question") :
val selectedCardColor by remember {
mutableStateOf(Color(0xFF00FFFF))
}
val unselectedCardColor by remember {
mutableStateOf(Color(0xAA007777))
}
// This is relevant to the question
var hmsToDecColor by remember {
mutableStateOf(selectedCardColor)
}
// This is relevant to the question
var decToHmsColor by remember {
mutableStateOf(unselectedCardColor)
}
...
Card(
modifier = Modifier
.weight(0.5f)
.padding(1.dp)
.clickable {
// This is relevant to the question
hmsToDecColor = unselectedCardColor
decToHmsColor = selectedCardColor
onClick(true)
},
border = BorderStroke(1.dp, Color.Black),
elevation = 4.dp
) {
Text(
text = str1,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.onSecondary,
modifier = Modifier
.background(hmsToDecColor) // This is relevant to the question
.padding(20.dp)
)
}
(There is another one that toggles the other way).
Everything works as planned. When I hit the card the color changes but... When I rotate the phone the colors are back to the default as if nothing was done.
I can not figure out why the "remember" does not remember...
I understand that everything is recomposed but expected the "remember" to persist.
I assume that the variables do remember but the fresh recomposition ignores the remembered value and uses the coded ones. If this is the case, how do I overcome this?
I understand that architecturally this is definitely not a good thing to do, but I have embedded a for loop in a composable to update state as follows:
#Composable
fun WorkScreen(name: String?) {
var text by remember {
mutableStateOf(0)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "YOU PRESSED ME $text")
}
for (i in 1..100) {
text = i
}
}
My expectation is that when I switch to this screen the for loop should update the mutableState and hence cause a recomposition which causes the time to tick up. However, instead I just get YOU PRESSED ME 0 if I put the for loop below the Box function, or I get YOU PRESSED ME 100 if I put it above the Box function.
The following question: Why my composable not recomposing on changing value for MutableState of HashMap?, does seem to be quite similar, but I'm not sure how it applies here. It seems to me I am updating the text value to be i!
You shouldn't change view state directly from the composable view builder, because compose functions will be recalled often during recomposition, so your calculation will be repeated. You should use side effects instead.
If you need to show dynamic change of the value to user, then you should use animation, as Gabriele's answer suggests.
An other option is updating the value manually. Inside LaunchedEffect you can use suspend functions, so you can change the value with a needed delay:
LaunchedEffect(Unit) {
for (i in 1..100) {
delay(1000) // update once a second
text = i
}
}
You should use an animation where you define how often you want to update the text applying it with a side effect.
For example:
var targetValue by remember { mutableStateOf(0) }
val value by animateIntAsState(
targetValue = targetValue,
animationSpec = tween( durationMillis = 2000 )
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(text = "YOU PRESSED ME $value")
}
LaunchedEffect(Unit) {
targetValue = 100
}