android – Listening to SwipeableState.progress.fraction lags the app

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. eg: 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 gets’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

It doesn’t seem too much with that basic project, add lots of content on top of it and 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

Leave a Comment