I'm developing an app for a very limited hardware and decided to use jetpack compose.
The problem arises when I need to display a list of cards and the lazyrow used for it gets extremely laggy. For comparison, I picked up a sample project with a recyclerView and used it to display roughly the same list of cards and the scrolling is as smooth as can be. Is jetpack compose inherently slower than xml view or am I doing something wrong?
Compose code (I can't exactly share my code, but the card composable is just a card with some images, icons and text):
#Composable
fun mainComposable(){
...
cardList = remember{ arrayListOf(...) }
lazyList(cardList)
...
}
#Composable
fun lazyList(
cardList: List<CardContent>,
){
LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 32.dp)
) {
items(
items = cardList,
key = { it.id }) { item ->
CardComposable(
content = item
)
}
}
}
I've already spent some time searching so I found a lot of optimizations, like running in release mode, setting minifyEnabled and shrinkResources to true on build.gradle and android.enableR8.fullMode to true in gradle.properties, using keys on the LazyRow, etc. They helped, but the scrolling is still fundamentally slower than an equivalent xml view app with recyclerView.
Edit: Added CardComposable code
#Composable
fun CardComposable(
content: Content,
) {
Card(
shape = RoundedCornerShape(8.dp),
elevation = 1.dp,
modifier = Modifier
.width(216.dp)
.height(308.dp)
) {
Column {
Box(contentAlignment = Alignment.TopEnd) {
Image(
painter = painterResource(id = content.image),
modifier = Modifier
.width(216.dp)
.height(164.dp)
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.padding(8.dp)
) {
Image(
painter = painterResource(id = R.drawable.button_background),
modifier = Modifier
.width(40.dp)
.height(40.dp)
)
Icon(
painter = painterResource(id = R.drawable.button),
modifier = Modifier
.width(16.dp)
.height(16.dp),
tint = GenericRedColor
)
}
}
Column(
modifier = Modifier
.padding(start = 16.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = content.Name,
maxLines = 2,
color = GenericBlackColor,
fontSize = 16.sp,
fontWeight = FontWeight(500),
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.width(134.dp)
.padding(top = 16.dp)
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.padding(start = 16.dp, top = 8.dp)
) {
Icon(
painter = painterResource(id = R.drawable.square_button),
modifier = Modifier
.width(32.dp)
.height(32.dp),
tint = GenericRedColor
)
Icon(
painter = painterResource(id = R.drawable.button_icon),
modifier = Modifier
.width(16.dp)
.height(16.dp),
tint = GenericWhiteColor
)
}
}
Text(
text = content.contentType,
color = GenericLightGrayColor2,
fontSize = 14.sp,
modifier = Modifier.padding(top = 8.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(26.dp),
modifier = Modifier.padding(top = 24.dp)
) {
val iconModifier = Modifier
.padding(end = 4.dp)
.width(12.dp)
.height(12.dp)
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(R.drawable.icon_1),
modifier = iconModifier,
tint = GenericLightGrayColor2
)
Text(
text = content.text_1,
fontSize = 14.sp,
color = GenericLightGrayColor2
)
Icon(
painter = painterResource(R.drawable.icon_2),
modifier = iconModifier,
tint = GenericLightGrayColor2
)
Text(
text = content.text2,
fontSize = 14.sp,
color = GenericLightGrayColor2
)
Icon(
painter = painterResource(R.drawable.icon_3),
modifier = iconModifier,
tint = GenericLightGrayColor2
)
Text(
text = content.text3,
fontSize = 14.sp,
color = GenericLightGrayColor2
)
}
}
}
}
}
}
Jetpack Compose is a separate library and is not included in the Android Operating System. Hence the code from the library should be Just In Time (JIT) compiled on the first run. This makes it inherently slower than Android View based code which is Ahead Of Time compiled(AOT) and binaries are stored inside the OS on the device.
This design decision of making Jetpack Compose as a standalone library has its advantages too. It makes it easier to update and use different versions of the library irrespective of the Android OS version, and enables backwards compatibility between compose and android versions.
In iOS, Swift takes the other approach and the Swift binaries are Ahead Of Time compiled and included in the OS. This is one of the main reasons other than Apple's laziness that prevents backwards compatibility in iOS.
Regarding the performance differences between RecyclerView and LazyLists, LazyLists are considerably less performant than RecyclerView. This has multiple reasons. I think it's mainly because Compose is a newer library and is constantly improving. The performance of earlier versions of LazyLists were considerably worse. Performance would be further improved in the upcoming compose versions.
For the time being, Since Jetpack Compose has interoperability with Android View based code, you can use RecyclerView in Compose with minimal performance overhead. Using AndroidView() function in Jetpack Compose.
#Composable
fun MyView(data: State<List<Item>>) {
//This function enables Compose to interop with View based code.
AndroidView(
factory = { context ->
RecyclerView(context).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
layoutManager = LinearLayoutManager(context)
adapter = ItemListAdapter().also { it.submitList().value }
}
},
update = { recyclerView ->
//Callback that runs on each recomposition.
}
)
}
Related
I have the following composable that represents a list model -
#Composable
fun DashboardCard(
modifier: Modifier = Modifier,
model: DashboardCardModel,
onCardClicked: (model: DashboardCardModel) -> Unit
) {
Column(
modifier = modifier
.size(200.dp, 200.dp)
.background(Color.Transparent)
.padding(16.dp)
.clickable {
onCardClicked(model)
},
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
if (model.showDefaultThumbnail) {
AsyncImage(
modifier = Modifier
.size(90.dp)
.clip(RoundedCornerShape(10.dp)),
model = model.thumbnailUrl, contentDescription = ""
)
} else {
Image(
modifier = Modifier
.size(90.dp)
.clip(RoundedCornerShape(10.dp)),
painter = painterResource(id = com.tinytap.R.drawable.tinytap),
contentDescription = ""
)
}
Image(
modifier = Modifier
.size(25.dp)
.padding(top = 10.dp)
.alpha(if (model.isCurrentPostOfInterest) 1f else 0f),
painter = painterResource(id = com.tinytap.R.drawable.post_of_interest),
contentDescription = null
)
Text(
modifier = Modifier.padding(top = 10.dp),
fontSize = 16.sp,
color = Color.White,
text = model.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = model.author,
fontSize = 12.sp,
color = Color.LightGray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
The issue is that I am using a fixed size for both my Image and AsyncImage but sometimes the API gives me images that are very wide, causing me to have the following inconsistency in the UI -
How can I make it that my images show up exactly the same? I tried using all kind of crops but the results ended up messing my image
Putting into consideration that image might be shorter, taller, wider or smaller.
To solve this issue, I recommend you to use Coil here is a sample of code that will solve your issue :
Card(
modifier = Modifier.size(119.dp, 92.dp),
shape = RoundedCornerShape(10.dp)
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(imageURL)
.build(),
placeholder = painterResource(R.drawable.complex_placeholder),
error = painterResource(R.drawable.complex_placeholder),
contentDescription = "complex image",
contentScale = ContentScale.Crop,//The line that will affect your image size and help you solve the problem.
)
}
The above code will always show fully covered box with image for any case, also don't forget to determine size of container (Card in my example '119,92' ).
To know more about different attributes and their effect on your code, select what suits you the best
Check more here (reference of image attached) : Content scaletype fully illustrated
I am creating a simple app in Compose with the following layout:
val viewModel: HomeScreenViewModel = viewModel()
val scrollState = rememberScrollState()
Column(
modifier = Modifier.verticalScroll(scrollState).fillMaxSize()
) {
Text(
"Editor picks".uppercase(),
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(8.dp),
)
ElevatedCard(
modifier = Modifier.clickable {
}.fillMaxSize()
) {
Column {
AsyncImage(
viewModel.topVideo?.thumbnailSrc,
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}
}
}
The output looks like so:
The image is not taking up all of the space which I need, it only works if I remove the Modifier.verticalScroll, so that the topmost element is just a simple column. When I do that, the output is what I expected:
I am extremely confused as to why this is happening, as I did not believe that making the root element scrollable would have any effect on the output.
I tried to look for a couple of hours online for solutions and I didn't find anything or any info as to why applying this modifier is ruining the layout.
This other person asked a similar question but the answer didn't work.
If you want your image to fill the entire screen width maintaining the correct aspect ratio, you can set contentScale to ContentScale.FillWidth for the Image composable.
Column(
modifier = Modifier.verticalScroll(scrollState)
) {
Text(
"Editor picks".uppercase(),
modifier = Modifier.padding(8.dp),
)
Card(
modifier = Modifier.clickable {}
) {
Column {
AsyncImage(
model = viewModel.topVideo?.thumbnailSrc,
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
}
}
I'm trying to build a custom Drop down menu and I encountered some issues in animating its state. The animation is both laggy and sketchy, even on a real device and even on a release build (APK). The Compose version I'm using is 1.1.1.
Observe the flicker (and the lag?).
The code:
Column(modifier = Modifier.fillMaxWidth()) {
var visible by remember { mutableStateOf(false) }
//header
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { visible = !visible }
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Click me",
style = MaterialTheme.typography.h6
)
Icon(
modifier = Modifier.rotate(animateFloatAsState(if (visible) 180f else 0f).value),
imageVector = Icons.Default.KeyboardArrowDown,
contentDescription = null
)
}
Divider(
modifier = Modifier.fillMaxWidth(),
color = Color.Black.copy(ContentAlpha.disabled)
)
}
//the 4 items
Column {
(1..4).forEach {
AnimatedVisibility(
visible = visible,
enter = expandVertically(
spring(
stiffness = Spring.StiffnessLow,
visibilityThreshold = IntSize.VisibilityThreshold
),
),
exit = shrinkVertically(),
) {
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = "Hello",
style = MaterialTheme.typography.h6
)
Icon(
imageVector = Icons.Default.KeyboardArrowRight,
contentDescription = null
)
}
Divider(
modifier = Modifier.fillMaxWidth(),
color = Color.Black
)
}
}
}
}
}
If I add some bottom padding to the bigger Column or if I make it occupy the whole screen's height, there's no more flicker, but I feel like that is a workaround and also I'm not sure whether the animation is lagging or not, so this wouldn't be a solution to all my problems. The parent Column wraps around its content and as the content size increases, it tries to "keep up" with the new size, but it doesn't do a perfect job. Am I using AnimatedVisibility improperly? How else could I create a custom Drop down menu?
i am new to jetpack compose and also have some experience with flutter so when move to compose its a little bit similar to flutter declarative UI style but i don't quite understand
like in flutter when i want some widget to expanded and want it to take full available space we can use Exapanded widget and if i use this in Column its automatically add flex of 1 or weight in terms of jetpack compose so i want to do same thing in jetpack compose but its seems like jetpack column not automatically and weight to its children and also i cant add
Modifier.weight(1f) in Card of NetworkCardComposable so i have to pass Modifier from as Parameter and in when using this Compoasable in columns this Modifier.weight(1f) have to set through parameter which is odd i think.
#Preview(showBackground = true, widthDp = 500, heightDp = 200)
#Composable
fun NetworkCardComposable(modifier: Modifier = Modifier) {
Card(
modifier = modifier
.fillMaxSize(1f)
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
backgroundColor = colorResource(id = R.color.jazz_color_primary)
) {
Row {
Card(
modifier = Modifier
.weight(2f)
.fillMaxSize()
.padding(8.dp),
shape = RoundedCornerShape(16.dp),
) {
Image(
modifier = Modifier
.padding(8.dp),
painter = painterResource(R.drawable.jazz_logo),
contentDescription = null,
)
}
Row(
modifier = Modifier
.weight(2f)
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
text = "Bundles & Offers",
color = Color.White,
textAlign = TextAlign.Center,
fontSize = MaterialTheme.typography.h5.fontSize,
)
Icon(
modifier = Modifier.size(56.dp),
imageVector = Icons.Default.ChevronRight,
tint = Color.White,
contentDescription = null,
)
}
}
}
}
#Preview(showBackground = true, showSystemUi = true)
#Composable
fun networkCard() {
Column(Modifier.fillMaxSize()) {
NetworkCardComposable(Modifier.weight(1f))
NetworkCardComposable(Modifier.weight(1f))
NetworkCardComposable(Modifier.weight(1f))
NetworkCardComposable(Modifier.weight(1f))
}
}
all thought i have acheive my desired layout but if there is any better approach to this please answer
Jetpack Compose aims at providing the maximum control over the UI. Hence, by default, the elements of a container like column default to wrap content. If you want them to occupy Max available space of the container, the proper way to do it is using Modifier.fillMaxSize()
As far as weights are concerned, if you do not specify then explicitly while adding more than one elements inside a container, the weights are calculated accordingly, based on the minimum required dimensions, (i.e., again, wrapContentSize ())
Hi i'm trying to implement a lazycolumn of a list of posts, I tested it on the emulator api 21 and 29 and it looks kinda smooth on the api 29 it's a little bit laggy, when I tested it on a physical device it was lagging, It looks like it's skipping frames or something..
I tried to remove some views that uses imageVector to see if that was the problem and still the same problem.
This is my composable view:
#Composable
fun HomePostView(
category: String,
imagesUrl: List<String> = listOf(imageHolder),
doctorProfileImage: String = imageUrl,
title: String,
subTitle: String
) {
Card(
shape = PostCardShape.large, modifier = Modifier
.padding(horizontal = 3.dp)
.fillMaxWidth()
) {
Column {
PostTopView(
category = category,
onOptionsClicked = { /*TODO option click*/ },
onBookmarkClicked = {/*TODO bookmark click*/ })
CoilImage(
data = imagesUrl[0],
fadeIn = true,
contentDescription = "post_image",
modifier = Modifier
.fillMaxWidth()
.requiredHeight(190.dp)
.padding(horizontal = contentPadding),
contentScale = ContentScale.Crop
)
Spacer(modifier = Modifier.height(10.dp))
PostDoctorContent(
doctorProfileImage = doctorProfileImage,
title = title,
subTitle = subTitle
)
Spacer(modifier = Modifier.height(contentPadding))
PostBottomView(likesCount = 293, commentsCount = 22)
Spacer(modifier = Modifier.height(contentPadding))
}
}
Spacer(modifier = Modifier.height(10.dp))
}
#Composable
private fun PostDoctorContent(doctorProfileImage: String, title: String, subTitle: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = contentPadding)
) {
CoilImage(data = doctorProfileImage,
contentScale = ContentScale.Crop,
contentDescription = null,
fadeIn = true,
modifier = Modifier
.size(30.dp)
.clip(CircleShape)
.clickable {
/*Todo on doctor profile clicked*/
})
Column {
Text(
text = title, fontSize = 14.sp, maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = contentPadding)
)
Text(
text = subTitle,
fontSize = 11.sp,
color = LightTextColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = contentPadding)
)
}
}
}
#Composable
private fun PostBottomView(likesCount: Long, commentsCount: Long) {
Row(
modifier = Modifier.padding(horizontal = contentPadding),
verticalAlignment = Alignment.CenterVertically
) {
Row(
Modifier
.clip(RoundedCornerShape(50))
.clickable { /*Todo on like clicked*/ }
.padding(5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_heart),
contentDescription = "Like"
)
Spacer(modifier = Modifier.width(5.dp))
Text(text = likesCount.toString(), fontSize = 9.sp)
}
Spacer(Modifier.width(20.dp))
Row(
Modifier
.clip(RoundedCornerShape(50))
.clickable { /*Todo on comment clicked*/ }
.padding(5.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_comment),
contentDescription = "Comment"
)
Spacer(modifier = Modifier.width(5.dp))
Text(text = commentsCount.toString(), fontSize = 9.sp)
}
}
}
#Composable
private fun PostTopView(
category: String,
onOptionsClicked: () -> Unit,
onBookmarkClicked: () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = onOptionsClicked) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_threedots),
contentDescription = "Options",
tint = Color.Unspecified
)
}
Text(text = category, fontSize = 16.sp, color = LightTextColor)
}
IconButton(onClick = onBookmarkClicked) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_bookmark),
contentDescription = "Bookmark"
)
}
}
}
and the lazyColumn:
LazyColumn(contentPadding = paddingValues , state = state ) {
item {
Spacer(modifier = Modifier.height(10.dp))
DoctorsList(
viewModel.doctorListData.value,
onCardClicked = {})
}
items(30) { post ->
HomePostView(
category = "Public Health ",
title = "Food Importance",
subTitle = "you should eat every day it's healthy and important for you, and drink water every 2 hours and what you should do is you should run every day for an hour"
)
}
}
Note: I'm still not using a viewmodel i'm just testing the view with fake data
This may not work for anyone else but on an earlier version (1.0.0-beta01) I saw a very large improvement in performance when I switched
lazy(items) { item ->
...
}
to
items.forEach { item ->
lazy {
...
}
}
I have no idea why and I'm not sure if this is still the case in later versions but its worth checking. So for the example given in the question, that would mean changing
items(30) {
...
}
to
repeat(30) {
item {
...
}
}
TLDR: Make sure your new LazyColumn compose element is not within a RelativeLayout or LinearLayout.
After some investigation the solution to this issue for us was the view in which the LazyColumn was constrained.
Our project uses a combination of Jetpack Compose and the older XML layout files. Our new compose elements were embedded within a RelativeLayout of an existing XML file. This was where the problem was. The compose element would be given the entire screen and then the onMeasure function of the compose element was called to re-configure the view and add our bottom nav bar...this onMeasure was called over and over again, which also in the case of a LazyColumn the re-measuring was throwing out the cache as the height had changed.
The solution for us was to change the RelativeLayout that contained both the new compose element and the bottom nav bar and replace it with a ConstraintLayout. This prevented the onMeasure from being called more than twice and gave a massive performance increase.
Try to build release build with turn off debug logs. should be works fine.
Ok, So far I know that there is an issue with the API when it comes to performance ...But what I found is this
Actually, In my case, I was just loading the image with 2980*3750 pixels image. I just crunched my resources to shorter pixels by some other tools
Now the lag is not present...
In my case, after I set the height of ComposeView to a specific value, it make LazyColumn scroll smooth.
Therefore, I create a XML file like
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.compose.ui.platform.ComposeView
android:id="#+id/compose"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle ? ) {
super.onViewCreated(view, savedInstanceState)
view.doOnLayout {
compose.layoutParams.height = view.height
compose.requestLayout()
}
}
I know ComposeView height already match_parent so set the height for it again on Fragment seem useless. However, without setting the height, LazyColumn will lagging when scroll.
I am not sure if it will work in all device but work well on my Pixel and Xiomi.
If you are using JPGs or PNGs in your project then check for their size, images having larger size causes a lots of lagging issues on low end devices.
I was having the same issue with my simple LazyColumn list and turned out I was using a JPG having size larger than 2MBs.