androidkotlinandroid-jetpack-compose

LazyColumn causing lag due to calculations for each item - How can I optimize it?


I'm experiencing significant lag when displaying a LazyColumn in my Jetpack Compose application. Each item involves some calculations (e.g., calculating the remaining time), and I load images asynchronously using Coil. This is causing performance issues, especially during scrolling.

Here's the relevant code:

I have a FeedViewState data class and a FeedViewModel that fetches a paginated list of Poll objects from the server. The LazyColumn displays this list, and when the user scrolls close to the end, I fetch more data (pagination).

@Composable
fun FeedComp(
    mainViewModel: MainViewModel,
    mainHomeVM: MainHomeViewModel,
    showBottomSheet: MutableState<Boolean>,
    navControllerMain: NavController,
) {
    // Initial data fetch
    LaunchedEffect(Unit) {
        feedVM.GetFeedFunctionality(mainViewModel)
    }

    val listState = rememberLazyListState()

    Scaffold(
        modifier = Modifier.blur(if (showBottomSheet.value) 15.dp else 0.dp),
        topBar = {
            MainHomeTopBarComp(mainViewModel = mainViewModel, showBottomSheet)
        },
    ) { paddingValues ->
        Box(Modifier.fillMaxSize()) {
            LazyColumn(
                state = listState,
                modifier = Modifier
                    .fillMaxSize()
                    .padding(paddingValues)
                    .background(Color.White),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                item { Spacer(modifier = Modifier.height(8.dp)) }
                items(state.FeedList) { poll ->
                    PollItem(
                        poll = poll,
                        mainViewModel = mainViewModel,
                        navControllerMain = navControllerMain,
                        pollReportId = pollReportId,
                        showManagementDialog = showManagementDialog,
                        managementOwner = managementOwner,
                        feedVM = feedVM
                    )  // Each PollItem involves calculations and Coil image loading
                }
            }
        }
    }

    // Pagination - fetch more data when user scrolls close to the end
    LaunchedEffect(listState.firstVisibleItemIndex) {
        val visibleItemCount = listState.layoutInfo.visibleItemsInfo.size
        val totalItemCount = listState.layoutInfo.totalItemsCount
        val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0

        if (lastVisibleItemIndex >= totalItemCount - 3 && !state.loading) {
            feedVM.GetFeedFunctionality(mainViewModel)
        }
    }
}

ViewModel and Data:

data class FeedViewState(
    val loading: Boolean = false,
    var FeedList: List<Poll> = emptyList(),
)

@HiltViewModel
class FeedViewModel @Inject constructor(
    private val feedRepository: FeedRepository,
) : ViewModel() {
    private val _state = MutableStateFlow(FeedViewState())
    val state = _state.asStateFlow()

    private var currentPage by mutableStateOf(1)

    fun GetFeedFunctionality(mainViewModel: MainViewModel) {
        if (state.value.loading) return

        val pageSize = 10
        val token = "Bearer ${mainViewModel.GetToken()}"
        viewModelScope.launch {
            _state.update { it.copy(loading = true) }
            feedRepository.getFeed(token = token, pageSize = pageSize, pageNumber = currentPage)
                .onRight { response ->
                    if (response.isSuccessful) {
                        response.body()?.data?.polls?.let { polls ->
                            _state.update {
                                it.copy(
                                    FeedList = it.FeedList + polls
                                )
                            }
                            currentPage++
                        }
                    }
                }
                .onLeft { error ->
                    Log.e("FeedViewModel", "Network error: ${error.error.message}")
                }
            _state.update { it.copy(loading = false) }
        }
    }
}

PollItem Composable: Each PollItem involves some calculations and image loading using Coil.

each poll(object) is a data class that i gotten from api

data class Poll(
    val commentCloesed: Boolean,
    val createdAt: String,
    val endedAt: String,
    val id: String,
    var isPollUp: Boolean,
    var likes: Int,
    val maximumParticipate: Int,
    var numberofcommnets: Int,
    var numberofshare: Int,
    val numberofviews: Int,
    val options: List<Option>,
    val owner: Owner?,
    val isOwner:Boolean,
    var participate: Int,
    val pollUrl: String,
    val polltype: Int,
    val question: String,
    val isAlphaUser:Boolean,
    val isAlpha:Boolean,
    val isSensitive:Boolean,
    var status: Int,
    val tags: List<String>,
    val topics: List<Topic>,
    var user: List<User>,
    var userCommentedPoll: Boolean,
    var userLike: Boolean,
    var userReShare: Boolean,
    val userViewedPoll: Boolean,
    var userVotedOnPoll: Boolean,
    val votingtype: Int,
    var deleted :Boolean = false
)
@Composable
fun PollItem(
    poll: Poll,
    mainViewModel: MainViewModel,
    navControllerMain: NavController,
    pollReportId: MutableState<String>,
    showManagementDialog: MutableState<Boolean>,
    managementOwner: MutableState<Boolean>,
    feedVM: FeedViewModel
) {
    val screenHeight = LocalConfiguration.current.screenHeightDp.dp
    val screenWidth = LocalConfiguration.current.screenWidthDp.dp
    if (!poll.deleted) {
        Column(
            Modifier
                .fillMaxWidth()
                .padding(end = 16.dp, start = 16.dp, bottom = 8.dp)
                .clip(RoundedCornerShape(4.dp))
                .background(backgroundpollcard)
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            mainViewModel.rememberedPoll.value = poll
                            navControllerMain.navigate("pollPage/${poll.id}")
                        },
                        onLongPress = {
                            pollReportId.value = poll.id
                            managementOwner.value = poll.isOwner
                            showManagementDialog.value = true

                        }
                    )
                }
                .padding(16.dp)
        ) {
            //polled up by who
            if (poll.isPollUp) {
                Row(
                    Modifier.fillMaxWidth(),
                    verticalAlignment = Alignment.CenterVertically,
                    horizontalArrangement = Arrangement.SpaceBetween
                ) {
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .clip(RoundedCornerShape(4.dp))
                            .border(1.dp, textSecondary, RoundedCornerShape(4.dp))
                            .background(backgroundTabs)
                            .padding(8.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        Row(
                            Modifier.fillMaxWidth(),
                            verticalAlignment = Alignment.CenterVertically
                        ) {
                            Icon(
                                painter = painterResource(id = R.drawable.unpollup),
                                contentDescription = "pollup icon",
                                tint = textSecondary,
                                modifier = Modifier
                                    .size(20.dp)
                            )
                            Spacer(modifier = Modifier.width(8.dp))
                            Text(
                                fontFamily = sfPro,
                                text = "PollUpped by ",
                                fontSize = 12.sp,
                                //letterSpacing = (-0.41).sp,
                                lineHeight = 14.sp,
                                fontWeight = FontWeight(400),
                                color = textSecondary
                            )
                            val currentUserId =
                                mainViewModel.singedInProfileResponse.value?.data?.id
                            val userName =
                                poll.user.find { it.userId == currentUserId }
                                    ?.let { "You" }
                                    ?: poll.user.firstOrNull()?.userName
                                    ?: poll.user[0].userName
                            Text(
                                modifier = Modifier
                                    .sizeIn(maxWidth = 100.dp)
                                // .background(Color.LightGray)
                                ,
                                overflow = TextOverflow.Ellipsis,
                                fontFamily = sfPro,
                                text = "$userName",
                                maxLines = 1,
                                fontSize = 12.sp,
                                //letterSpacing = (-0.41).sp,
                                lineHeight = 14.sp,
                                fontWeight = FontWeight(600),
                                color = textSecondary
                            )
                            if (poll.user.size > 1) {
                                Text(
                                    fontFamily = sfPro,
                                    text = " and ",
                                    fontSize = 12.sp,
                                    //letterSpacing = (-0.41).sp,
                                    lineHeight = 14.sp,
                                    fontWeight = FontWeight(400),
                                    color = textSecondary
                                )
                                Text(
                                    overflow = TextOverflow.Ellipsis,
                                    fontFamily = sfPro,
                                    text = "${poll.user.size - 1}",
                                    maxLines = 1,
                                    fontSize = 12.sp,
                                    //letterSpacing = (-0.41).sp,
                                    lineHeight = 14.sp,
                                    fontWeight = FontWeight(600),
                                    color = textSecondary
                                )
                                Text(
                                    fontFamily = sfPro,
                                    text = "${if (poll.user.size - 1 > 1) "others" else "other"}",
                                    fontSize = 12.sp,
                                    //letterSpacing = (-0.41).sp,
                                    lineHeight = 14.sp,
                                    fontWeight = FontWeight(400),
                                    color = textSecondary
                                )

                            }
                        }

                    }
                }
                Spacer(modifier = Modifier.height(24.dp))
            }

            //profile & progress
            Row(
                Modifier.fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Box(
                        modifier = Modifier
                            .size(40.dp)
                            .clip(RoundedCornerShape(100))
                            .background(Color.LightGray)
                    ) {
                        if (poll.owner != null) {
                            if (poll.owner.pictureUrl != null && poll.owner.pictureUrl != "") {
                                AsyncImage(
                                    modifier = Modifier.fillMaxSize(),
                                    model = poll.owner?.pictureUrl,
                                    contentDescription = "poll owner profile image",
                                    contentScale = ContentScale.Crop
                                )
                            } else {
                                Image(
                                    painter = painterResource(id = R.drawable.defaultprof),
                                    modifier = Modifier.fillMaxSize(),
                                    contentDescription = "poll owner profile image",
                                    contentScale = ContentScale.Crop
                                )
                            }

                        } else {
                            Image(
                                painter = painterResource(id = R.drawable.anonymous_emblem),
                                modifier = Modifier.fillMaxSize(),
                                contentDescription = "poll owner profile image",
                                contentScale = ContentScale.Crop
                            )
                        }

                    }
                    Spacer(modifier = Modifier.width(8.dp))
                    if (poll.owner != null) {
                        Column(Modifier.offset(y = -2.dp)) {
                            Row(verticalAlignment = Alignment.CenterVertically) {
                                poll.owner?.firstName?.let {
                                    Text(
                                        fontFamily = sfPro,
                                        text = it,
                                        fontSize = 16.sp,
                                        //letterSpacing = (-0.41).sp,
                                        lineHeight = 19.sp,
                                        fontWeight = FontWeight(500),
                                        color = textSecondary
                                    )
                                }
                                if (poll.isAlphaUser) {
                                    Spacer(modifier = Modifier.width(8.dp))
                                    Image(
                                        painter = painterResource(id = R.drawable.alpha),
                                        contentDescription = "alpha sign",
                                        modifier = Modifier
                                            .size(17.dp)
                                    )

                                }
                            }

                            Spacer(modifier = Modifier.height(8.dp))
                            Text(
                                fontFamily = sfPro,
                                text = "@" + poll.owner?.userName,
                                fontSize = 10.sp,
                                // letterSpacing = (-0.41).sp,
                                lineHeight = 11.sp,
                                fontWeight = FontWeight(500),
                                color = textThirt
                            )
                        }
                    } else {
                        Text(
                            fontFamily = sfPro,
                            text = "Anonymous Poll",
                            fontSize = 16.sp,
                            //letterSpacing = (-0.41).sp,
                            lineHeight = 19.sp,
                            fontWeight = FontWeight(500),
                            color = textSecondary
                        )
                        //todo:inja YOU biyad , fix condition
                        if (poll.isOwner == true) {
                            Spacer(modifier = Modifier.width(8.dp))
                            Text(
                                modifier = Modifier
                                    .border(
                                        0.5.dp,
                                        color = textSecondary,
                                        RoundedCornerShape(4.dp)
                                    )
                                    .padding(
                                        start = 6.dp,
                                        end = 6.dp
                                    ),
                                fontFamily = sfPro,
                                text = "You",
                                fontSize = 8.sp,
                                //letterSpacing = (-0.41).sp,
                                lineHeight = 19.sp,
                                fontWeight = FontWeight(500),
                                color = textSecondary
                            )
                        }

                    }


                }

                Box(contentAlignment = Alignment.Center) {
                    if (poll.status == 1) {
                        CircularProgressIndicator(
                            modifier = Modifier.size(40.dp),
                            strokeWidth = 2.dp,
                            color = feedVM.timeRemaining(poll.endedAt).second,
                            trackColor = strokesVoteTime,
                            strokeCap = StrokeCap.Round,
                            progress = {
                                feedVM.calculatePercentage(
                                    startDate = poll.createdAt,
                                    endDate = poll.endedAt
                                )
                            }
                        )

                        Box {
                            Text(
                                fontFamily = sfPro,
                                text = "${feedVM.timeRemaining(poll.endedAt).first}",
                                fontSize = 10.sp,
                                //letterSpacing = (-0.41).sp,
                                lineHeight = 12.sp,
                                fontWeight = FontWeight(500),
                                color = if (poll.status == 0) textthird else feedVM.timeRemaining(
                                    poll.endedAt
                                ).second
                            )
                        }
                    } else {
                        CircularProgressIndicator(
                            modifier = Modifier.size(40.dp),
                            strokeWidth = 2.dp,
                            color = textthird,
                            trackColor = strokesVoteTime,
                            strokeCap = StrokeCap.Round,
                            progress = {
                                1f
                            }
                        )

                        Box {
                            Text(
                                fontFamily = sfPro,
                                text = "Ended",
                                fontSize = 10.sp,
                                //letterSpacing = (-0.41).sp,
                                lineHeight = 12.sp,
                                fontWeight = FontWeight(500),
                                color = textthird
                            )
                        }
                    }

                }
            }
            Spacer(modifier = Modifier.height(16.dp))
            //question
            Box(modifier = Modifier.fillMaxWidth()) {
                HashtagText(
                    text = poll.question,
                    onHashtagClick = { hashtag ->
                        navControllerMain.navigate("pollPage/${poll.id}")
                        println("Hashtag clicked: $hashtag")
                    },
                    onTextClick = {
                        navControllerMain.navigate("pollPage/${poll.id}")
                        println("Non-hashtag text clicked!")
                    }
                )
            }

            //first option
            if (poll.options.isNotEmpty()) {
                Spacer(modifier = Modifier.height(16.dp))
                if (poll.options[0].type == 0) {//means its text poll
                    Row(
                        Modifier.fillMaxWidth(),
                        verticalAlignment = Alignment.CenterVertically
                    ) {

                        CustomRadioButton(
                            isSelected = mutableStateOf(poll.options[0].userChoose),
                            onClick = {},
                            borderColor = textthird,
                            size = 16.dp,
                            dotColor = textthird,
                            isEnabled = false
                        )
                        Spacer(modifier = Modifier.width(8.dp))
                        Column {
                            Text(
                                fontFamily = sfPro,
                                text = poll.options[0].title,
                                fontSize = 14.sp,
                                //letterSpacing = (-0.41).sp,
                                lineHeight = 17.sp,
                                fontWeight = FontWeight(500),
                                color = textSecondary
                            )
                            Spacer(modifier = Modifier.height(4.dp))
                            Box(
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .height(2.dp)
                                    .background(if (poll.options[0].userChoose) textthird else strokesVoteTime)
                            )
                        }
                    }
                } else {//means its image poll
                    // Text(text = "${(screenHeight*9/ 40)}")
                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(screenHeight * 9 / 40)
                            .clip(RoundedCornerShape(4.dp))
                            .background(Color.LightGray)
                    ) {

                        AsyncImage(
                            model = poll.options[0].data,
                            contentDescription = null,
                            modifier = Modifier.fillMaxSize(),
                            contentScale = ContentScale.Crop
                        )


                    }
                }
            }

            //more option(s)
            Spacer(modifier = Modifier.height(8.dp))
            Row(Modifier.fillMaxWidth()) {
                Text(
                    fontFamily = sfPro,
                    text = "${poll.options.size - 1} more ${if (poll.options.size - 1 == 1) "option" else "options"}",
                    fontSize = 12.sp,
                    //letterSpacing = (-0.41).sp,
                    lineHeight = 15.sp,
                    fontWeight = FontWeight(400),
                    color = textthird
                )
            }

            //like comment
            Spacer(modifier = Modifier.height(16.dp))
            Row(
                Modifier.fillMaxWidth(),
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.SpaceBetween
            ) {

                //alpha
                if (poll.isAlpha) {
                    Text(
                        fontFamily = sfPro,
                        text = "Alpha",
                        fontSize = 12.sp,
                        //letterSpacing = (-0.41).sp,
                        lineHeight = 15.sp,
                        fontWeight = FontWeight(600),
                        color = textImportant
                    )
                }
                //comment
                if (!poll.commentCloesed) {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        Icon(
                            modifier = Modifier.size(17.dp),
                            painter = if (poll.userCommentedPoll)
                                painterResource(id = R.drawable.comment)
                            else
                                painterResource(id = R.drawable.uncomment),
                            tint = if (poll.status == 1) if (poll.userCommentedPoll) iconsFunctionalityComment else iconsFunctionalityDefault else textthird,
                            contentDescription = "icon"
                        )
                        Spacer(modifier = Modifier.width(4.dp))
                        Text(
                            fontFamily = sfPro,
                            text = "${poll.numberofcommnets}",
                            fontSize = 10.sp,
                            //letterSpacing = (-0.41).sp,
                            lineHeight = 12.sp,
                            fontWeight = FontWeight(400),
                            color = iconsFunctionalityDefault
                        )
                    }

                }

                //like
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Icon(
                        modifier = Modifier.size(17.dp),
                        painter = if (poll.userLike)
                            painterResource(id = R.drawable.like)
                        else
                            painterResource(id = R.drawable.unlike),
                        tint = if (poll.status == 1) if (poll.userLike) iconsFunctionalityLike else iconsFunctionalityDefault else textthird,
                        contentDescription = "icon"
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    Text(
                        fontFamily = sfPro,
                        text = "${poll.likes}",
                        fontSize = 10.sp,
                        //letterSpacing = (-0.41).sp,
                        lineHeight = 12.sp,
                        fontWeight = FontWeight(400),
                        color = iconsFunctionalityDefault
                    )
                }

                //pollup
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Icon(
                        modifier = Modifier.size(17.dp),
                        painter = if (poll.userReShare)
                            painterResource(id = R.drawable.pollup)
                        else
                            painterResource(id = R.drawable.unpollup),
                        tint = if (poll.status == 1) if (poll.userReShare) iconsFunctionalityPollup else iconsFunctionalityDefault else textthird,
                        contentDescription = "icon"
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    Text(
                        fontFamily = sfPro,
                        text = "${poll.numberofshare}",
                        fontSize = 10.sp,
                        //letterSpacing = (-0.41).sp,
                        lineHeight = 12.sp,
                        fontWeight = FontWeight(400),
                        color = iconsFunctionalityDefault
                    )
                }

                //voters
                Row(verticalAlignment = Alignment.CenterVertically) {
                    Icon(
                        modifier = Modifier.size(17.dp),
                        painter = if (poll.userVotedOnPoll)
                            painterResource(id = R.drawable.vote)
                        else
                            painterResource(id = R.drawable.unvote),
                        tint = if (poll.status == 1) if (poll.userVotedOnPoll) iconsFunctionalityVoter else iconsFunctionalityDefault else textthird,
                        contentDescription = "icon"
                    )
                    Spacer(modifier = Modifier.width(4.dp))
                    Text(
                        fontFamily = sfPro,
                        text = "${poll.participate}",
                        fontSize = 10.sp,
                        //letterSpacing = (-0.41).sp,
                        lineHeight = 12.sp,
                        fontWeight = FontWeight(400),
                        color = iconsFunctionalityDefault
                    )
                }

            }

        }
    }
}

My Issue: The LazyColumn causes lag, especially during scrolling. Each PollItem requires calculations, and images are loaded asynchronously using Coil.


Solution

  • The issue is resolved when I build the app in release mode. Debug mode was causing performance overhead, but switching to release mode fixed the lag and improved scrolling performance significantly.