2

I'm writing example-screen with using lazyColumn. I encountered some performance issues on release build. Frame skipping happens when I fast-scroll the list. All models and composables are stable. My code is below;
Screen record link -> https://i.stack.imgur.com/bd2B5.jpg

viewModel:

@HiltViewModel
class ExampleViewModel @Inject constructor(
    private val repo: ExampleRepository,
) : ViewModel() {

    private val _viewState = MutableStateFlow(ItemsViewState())
    val viewState = _viewState.asStateFlow()

    init {
        fetch()
    }

    private fun fetch() = viewModelScope.launch {
        repo.getItems()
            .onStart { _viewState.value = _viewState.value.copy(state = PageState.Loading) }
            .onCompletion { _viewState.value = _viewState.value.copy(state = PageState.Content) }
            .collect { _viewState.value = _viewState.value.copy(items = it.toImmutableList()) }
    }
}

viewState and models:

data class ItemsViewState(
    val items: ImmutableList<Item> = persistentListOf(),
    val state: PageState = PageState.Loading,
)

data class Item(
    val id: Int,
    val imageUrl: String,
    val name: String,
    val rating: Double,
    val campaignText: String,
    val isChecked: Boolean = false,
)

data class ItemViewState(val item: Item) {

    fun isRatingVisible(): Boolean = item.rating > 7.0
}

sealed class PageState {

    object Content : PageState()

    object Loading : PageState()

    object Error : PageState()
}

and my composable functions:

@Composable
fun ExampleScreen(
    viewModel: ExampleViewModel = hiltViewModel(),
) {
    val viewState by viewModel.viewState.collectAsState()

    when (viewState.state) {
        PageState.Content -> {
            ExampleList(viewState = viewState)
        }
        PageState.Loading -> LoadingScreen()
        PageState.Error -> {}
    }
}

@Composable
private fun ExampleList(
    viewState: ItemsViewState,
) {
    LazyColumn(
        state = rememberLazyListState(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        modifier = Modifier.fillMaxSize()
    ) {
        items(viewState.items, key = { it.id }) { item ->
            ExampleListItem(item = item)
        }
    }
}

@Composable
private fun ExampleListItem(item: Item) {
    val viewState = ItemViewState(item)

    Card(
        shape = RoundedCornerShape(8.dp),
        backgroundColor = MaterialTheme.colors.background
    ) {
        Row(
            modifier = Modifier
                .padding(8.dp)
                .fillMaxWidth()
                .wrapContentHeight()
        ) {
            AsyncImage(
                model = item.imageUrl,
                contentDescription = viewState.item.name,
                contentScale = ContentScale.FillHeight,
                modifier = Modifier
                    .clip(RoundedCornerShape(10.dp))
                    .width(120.dp)
                    .height(120.dp),
            )
            Spacer(modifier = Modifier.width(8.dp))
            Column(verticalArrangement = Arrangement.SpaceBetween) {
                Row(horizontalArrangement = Arrangement.SpaceEvenly) {
                    Text(
                        text = viewState.item.name,
                        overflow = TextOverflow.Ellipsis,
                        maxLines = 1,
                        modifier = Modifier.weight(1f),
                    )
                    Icon(imageVector = Icons.Default.List, contentDescription = null)
                }
                Spacer(modifier = Modifier.height(2.dp))
                Row {
                    if (viewState.isRatingVisible()) {
                        Spacer(modifier = Modifier.width(4.dp))
                        Text(
                            text = viewState.item.rating.toString(),
                            overflow = TextOverflow.Ellipsis,
                            maxLines = 1,
                        )
                    }
                }
                Spacer(modifier = Modifier.height(2.dp))
                CampaignRow(campaignText = viewState.item.campaignText)
            }
        }
    }
}

@Composable
private fun CampaignRow(
    modifier: Modifier = Modifier,
    campaignText: String,
) = Row(modifier = modifier) {
    Image(
        painter = painterResource(androidx.ui.livedata.R.drawable.abc_ic_star_black_16dp),
        contentDescription = "",
        modifier = Modifier
            .wrapContentSize()
            .align(Alignment.CenterVertically)
            .padding(end = 4.dp)
    )
    Text(
        text = campaignText,
        overflow = TextOverflow.Ellipsis,
        maxLines = 1,
    )
}

I followed google guideline to fix performance issues.

  • I created Baseline profile
  • I tested on release build
  • I used key parameter on lazyColumn

but still happening performance issues. How can I prevent this?

pcparticle
  • 123
  • 1
  • 6
  • My theory is that when you are doing `val viewState = ItemViewState(item)` then you might loose skipability, because viewState is pretty much derived state at this point. I don't see any point in `ItemViewState` in code you've given, but i might be missing some context. Either way i'd change that line to `val viewState = remember(item) ( ItemViewState(item) }` and see if that help. Other than that i'd guard the code with recomposition tracking (or just use new layout inspector features for that) to track what i going on there – Jakoss Dec 19 '22 at 13:51
  • Thanks for answer @Jakoss I'm not sure about the use of remember here. because when I watch the viewState hashCodes, I see that each item viewstate has the same hashcode. I still tried using remember but no change. There doesn't seem any problem when I watch the recomposing on layout inspector. Items recompose when I scroll, isn't this expected? – pcparticle Dec 19 '22 at 14:21
  • layoutInspector recomposition counts -> https://prnt.sc/RCjwvaHHYUJb @Jakoss – pcparticle Dec 19 '22 at 14:23
  • Items definitely shouldn't recompose when you scroll (this should only relayout things). That suggests that some of your state isn't stable – Jakoss Dec 19 '22 at 15:33
  • Other thing, why is your `CampaignRow` returning a composable? That should NEVER be a case. Composable that emits should return Unit. I'm not sure if that will change anything performance wise, but compose team stated that a lot – Jakoss Dec 19 '22 at 15:36
  • I'd start my investigation by getting rid of `ItemViewState`, because it's really unnecessary here and see if that helps – Jakoss Dec 19 '22 at 15:37
  • I usually keep logics in viewState. yes, it may seem unnecessary here, but I use it in most projects. That's why I used this. You're right the campaignrow function is misspelled. – pcparticle Dec 19 '22 at 16:40
  • In layoutInspector, only saveableItemProvider's recomposition count increased – pcparticle Dec 19 '22 at 16:49
  • Hey @pcparticle did you manage to solve your issue? – latsson Apr 03 '23 at 12:17
  • Hey, it's still going on :( @latsson – pcparticle Apr 04 '23 at 06:56
  • Thanks for the update. I'm having the same issue. No recompose happening while scrolling. Nobody in my team seems to be able to find the problem. – latsson Apr 05 '23 at 09:28

0 Answers0