Search code examples
androidoptimizationandroid-jetpack-composelazy-loadingandroid-xml

Jetpack Compose lazyrow considerably slower than a recyclerView on xml


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



Solution

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