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.
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.