Listening to SwipeableState.progress.fraction lags the app

468 views Asked by At

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.

0

There are 0 answers