2

A bit of previous context is good to understand the purpose and if there's a better solution or find the mistake here.

Basically I have a LazyColumn with nested scroll and swipeableState.

  • Nested Scroll is listening for delta values and use the delta value to make swipeableState.performDrag(deltaValue) to keep track of scroll quantity and then return Offset.Zero letting LazyColumn scroll as if nothing was there.

  • SwipeableState helps me to identify how much percentage has user scrolled is from 0 to X. e.g: anchor from 0 to 300.dp height. User has scrolled is on 150.dp. so basically I get swipeableState.progress.fraction which return 0.5000000f

What I pretend to do is to change alpha values/padding etc.. as percentage increses (Depending on state)

Alright that was the context, now the PROBLEM.

Whenever I listen for swipeableState.progress.fraction looks like the entire app lags. To make this example easier to get, I won't be chaning any alpha/padding as the fraction increases, so nothing get's recomposed. (They're all skipped, at least that's what the Android Studio IDE Dolphin told me)

By just listening swipeableState.progress.fraction it lags the app. (In release). As soon as it reaches the 1.0 it stops lagging. There must be something wrong, maybe something is getting recomposed and I haven't seen it.

Here's the code:

val swipeableFraction by remember {
    derivedStateOf {
        val formattedFraction = String.format("%.2f", swipeableState.progress.fraction).toFloat() // format to have 2 numbers like 0.82 instead of 0.823258142823
        formattedFraction
    }
}
val formattedValueToUse by remember(swipeableFraction) {
    mutableStateOf(swipeableFraction)
}

Is there a better solution or improvement? or am I missing some compose princple? I've been trying to understand what's happening and trying to find some solutions/debugging.

I really appreciate any response or help.

Thank you!

EDIT

Video with Profile HDWUI rendering enabled

  • 0:00 TO 0:15 working as expected
  • 0:15 TO 0:45 triggering the laggines
  • 0:45 TO 1:10 it stays laggy whenever swipeable is on action even though it's a simple scroll

Redmi note 8 PRO

https://www.youtube.com/watch?v=mOg9A7cLnMI

It doesn't seem too much with that basic project, but now think when you add lots of content on top of it and do some calculations that you would do with swipeableStateProgress to make some changes in the UI (Alpha, padding etc..) The lag stays forever while listening to swipeable state progress and the only way to remove it is navigating back or forward and comeback to the screen.

Reproducible sample:

In the MainActivty OnCreate add the next:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApplicationTheme {
                val swipeableState = rememberSwipeableState(initialValue = States.EXPANDED)
                val listState = rememberLazyListState()
                val isFirstVisibleItem by remember {
                    derivedStateOf {
                        listState.firstVisibleItemIndex == 0
                    }
                }

                val swipeableFraction by remember {
                    derivedStateOf {
                        val fraction = when {
                            swipeableState.progress.from == States.EXPANDED && swipeableState.progress.to == States.EXPANDED -> 0f
                            swipeableState.progress.from == States.COLLAPSED && swipeableState.progress.to == States.COLLAPSED -> 1f
                            else -> swipeableState.progress.fraction
                        }

                        fraction
                    }
                }

                val height = with(LocalDensity.current) {
                    firstItemHeight.toPx()
                }


                LazyColumn(
                    Modifier
                        .swipeable(
                            state = swipeableState,
                            orientation = Orientation.Vertical,
                            anchors = mapOf(
                                0f to States.COLLAPSED,
                                height to States.EXPANDED
                            )
                        )
                        .nestedScroll(
                            customNestedScroll(
                                isFirstItemVisible = isFirstVisibleItem,
                                onDeltaChange = { swipeableState.performDrag(it) }
                            )
                        )
                ) {
                    item {
                        Box(
                            Modifier
                                .fillMaxWidth()
                                .height(firstItemHeight)
                                .background(Color.Red)
                        )
                    }
                    stickyHeader {
                        Row() {
                          Text(text = ("Some text"))
                            Text(text = ("Some text 2"))
                            Text(text = ("Some text 3"))
                        }
                    }

                    item {
                        Box(modifier = Modifier
                            .size(200.dp)
                            .background(Color.Yellow)) {
                            Text(text = "${swipeableFraction}")
                        }
                    }

                    item {
                        TestTabOne()
                    }
                }
            }
        }
    }
}

In the same file but outside of MainActivity, add this:

enum class States {
    EXPANDED,
    COLLAPSED
}

@Composable
fun TestTabOne() {
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
             item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
        }
        LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            item { RandomBox() }
            item { RandomBox() }
        }

        LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
        }

        LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
            item { RandomBox() }
        }

        RandomBox()
        RandomBox()
        RandomBox()
        RandomBox()
        RandomBox()
        RandomBox()
        RandomBox()
        RandomBox()
        RandomBox()

    }
}

private fun customNestedScroll(
    isFirstItemVisible: Boolean,
    onDeltaChange: (Float) -> Unit
): NestedScrollConnection {
    return object : NestedScrollConnection {
        override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
            val delta = available.y
            if (delta.toCollapsed() && isFirstItemVisible) {
                onDeltaChange(delta)
            } else {
                // Only start performing drag on swipeable state whenever user status area is visible
                if (isFirstItemVisible) {
                    onDeltaChange(delta)
                }
            }
            return Offset.Zero
        }
    }
}

val listOfColor = listOf(Color.White, Color.Red, Color.Blue, Color.Yellow, Color.Green, Color.Cyan, Color.Magenta, Color.Black)
@Composable
fun RandomBox() {
    val colorToRemember by remember { mutableStateOf(listOfColor[Random.nextInt(7)]) }
    Box(
        Modifier
            .size(200.dp)
            .background(colorToRemember)
            .padding(10.dp))
}

private fun Float.toCollapsed() = this < 0
val firstItemHeight = 300.dp

Edit after 4 months: After using electric eel, if you use this example and also use Layout Inspector, you should notice how the entire LazyColumn gets recomposed (Not what it's inside the item, but the LazyScopeProviderImpl and something else) and this happens for every item.

Solution: Defer the read inside the item { } scope. And if there's a big giant composable there, then defer it more. Remember, there's one rule for situations that gets recomposed a lot, and is defer the value as long as possible.

Barrufet
  • 495
  • 1
  • 11
  • 33
  • 1
    Why is `formattedValueToUse` variable necessary at all? It seems every time its key `swipeableFraction` (which is already a remembered variable) changes you create a new `MutableState` that simply holds the same variable - `swipeableFraction`? Unless you mutate this variable elsewhere I can't see the point of it. – Mark May 29 '22 at 23:00
  • Mark is correct, but it shouldn't cause the lags. Please provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). – Phil Dukhov May 30 '22 at 02:47
  • Have you tried to profile which code exactly is choking the ui? I bet my money on String.format() that's inherently slow, because it allocates `Pattern` , compiles it and does all that matching. – Nikola Despotoski May 30 '22 at 09:45
  • I would use only `swipeableFraction` but if you don't use that variable then it's not triggered that's why I created another variable to trigger `swipeableFraction`. Will try to make a minimal reproducible example this afternoon. Also I've noticed that janky frames shows up after a while. To "clean it up" you can either press back or navigate up and then comeback to the screen. Then after scrolling up/down for 20-40 seconds appears again – Barrufet May 30 '22 at 14:11
  • @NikolaDespotoski I've tried to use profile but I wasn't able to get out of it (Probably a newbie). String.format is a post implementation after finding out that UI is junky, and a shot in the dark to try to solve it and not recompose every time. – Barrufet May 30 '22 at 14:13
  • I have lots of images, could that be the problem? I'm going to remove them. I'm using `Image()` composable – Barrufet May 30 '22 at 14:23
  • Removing Image(), didn't solve the problem – Barrufet May 30 '22 at 14:37
  • Hey I've been able to make a reproducible sample and a link to the video, please feel free to point out anything. But take into account that this is just a small part, if you add some kind of calculation when listening to swipeable progress the laggines increases – Barrufet Jun 07 '22 at 18:55
  • from my understanding `derivedStateOf` is used for converting stream into state (throttling/snapshot). When you directly want to use `progress.fraction` it's not necessary to `remember` it since you don't care about recomposition on every value stream. From my testing, it's the Text composable that is being drawn lots of times that is causing UI lag. In your use case, if you eventually gonna convert `progress.fraction` to state using `derivedState`, you shouldn't have any problem. – AagitoEx Jun 08 '22 at 11:24
  • Thanks for the answer @AagitoEx it makes total sense, wondering if is there any way to prevent that lag? Since I can reproduce this only with a Text being drawn lots of time then I guess it will worsen when there's lot of Texts being drawn at the same time. e.g: let's suppose I have 5 Texts, then alpha and padding properties. Maybe set alpha in `.graphicsLayer{}` will improve performance since it's done on Draw phase? What about padding and Text? – Barrufet Jun 08 '22 at 11:56
  • I think having lots of text doesn't matter as much as you are only initializing them once and you likely won't be recomposing every one of them every frame. If you really need the rendering prowess of a GPU you probably need to resort to using a custom SurfaceView implementation. [Compose vs Surface VIew](https://medium.com/mobile-app-development-publication/surfaceview-still-outperform-jetpack-compose-in-rendering-7c0aebd6fb0f) – AagitoEx Jun 09 '22 at 06:17

0 Answers0