Hi everyone, I'm working on an Android TV app usi...
# compose
r
Hi everyone, I'm working on an Android TV app using the latest stable Jetpack Compose. The home screen is a typical streaming layout: a "LazyColumn" containing multiple "LazyRow"s (similar to Netflix/Emby). I'm seeing noticeable jank while navigating with the D-pad and scrolling, even after the page has already been visited once. So this doesn't appear to be a cold-start issue (initial composition, image loading, etc.). The performance issue persists on subsequent navigation. I've already applied the common optimizations: - Stable "key"s for both "LazyColumn" and "LazyRow" - "contentType" provided - Stable data models as much as possible - Per-row "LazyListState" - Nested prefetch tuning - Images loaded at the correct display size - The issue is still present after images are cached My questions are: 1. Is "LazyColumn" + "LazyRow" still considered the recommended approach for this kind of TV UI? 2. Are there any known performance limitations with this pattern in the latest Compose? 3. What is the best way to determine whether I'm hitting Compose's performance ceiling versus having an optimization issue in my code? 4. Has anyone compared this with a traditional View implementation (e.g. "RecyclerView", or "RecyclerView" + "ComposeView")? Does switching away from Compose typically improve this kind of workload on Android TV? The project is currently private, but I'm happy to provide repository access to anyone willing to help investigate. I can also share the relevant code paths, Macrobenchmark results, Perfetto traces, Layout Inspector captures, or prepare a minimal reproducible sample if that would be more useful. Any suggestions or similar experiences would be greatly appreciated. Thanks!
t
Have you tried overriding the default
BringIntoViewSpec
? The default can sometimes make it look janky (or at least it did in earlier versions of compose)
Copy code
package com.example.scrolltest.ui.theme

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.spring
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.BringIntoViewSpec
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState

// Stiffness decides how smooth the scroll is (lower value = smoother and slower scroll)
private const val SCROLL_STIFFNESS = 100f

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun rememberBringIntoViewSpec(
    parentFraction: Float,
    childFraction: Float
): BringIntoViewSpec {
    // It's important that we reuse the same BringIntoViewSpec instance if these value changes, otherwise Compose seems to get confused and calls
    // both the old and the new BringIntoSpec in a loop. So pass updated values through rememberUpdatedState() instead
    val updatedParentFraction by rememberUpdatedState(parentFraction)
    val updatedChildFraction by rememberUpdatedState(childFraction)

    return remember {
        object : BringIntoViewSpec {
            // Taken from <https://issuetracker.google.com/issues/348896032>
            override fun calculateScrollDistance(
                // initial position of item requesting focus
                offset: Float,
                // size of item requesting focus
                size: Float,
                // size of the lazy container
                containerSize: Float
            ): Float {
                val childSmallerThanParent = size <= containerSize
                val initialTargetForLeadingEdge = updatedParentFraction * containerSize - (updatedChildFraction * size)
                val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge

                val targetForLeadingEdge = if (childSmallerThanParent && spaceAvailableToShowItem < size) {
                    containerSize - size
                } else {
                    initialTargetForLeadingEdge
                }

                return offset - targetForLeadingEdge
            }

            override val scrollAnimationSpec: AnimationSpec<Float>
                get() = spring(stiffness = SCROLL_STIFFNESS)
        }
    }
}
Then apply it to your theme:
Copy code
CompositionLocalProvider(
    LocalBringIntoViewSpec provides rememberBringIntoViewSpec(
        parentFraction = 0.5f,
        childFraction = 0.5f
    )
) {
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}