Search code examples
androidandroid-jetpack-composeandroid-jetpack-compose-lazy-column

How to compose items that are offscreen with LazyColumn or LazyRow?


This is a share your knowledge, Q&A-style question to create a follow up answer to create a Lazy list(Column, Row, Grid) that can compose more items than default behavior allows which is the next item in scroll direction when you start scrolling.

In cases, where some items take more time to load, like a video or downloading an image, so you might want to have more items that are loaded or entering composition offscreen.

enter image description here

LazyRow(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(8.dp),
    contentPadding = PaddingValues(16.dp),
) {
    items(30) {
        var loading by remember {
            mutableStateOf(true)
        }

        LaunchedEffect(Unit) {
            println("Composing First LazyRow item: $it")
            delay(1000)
            loading = false
        }
        MyRow(itemWidth, loading, it)
    }
}

@Composable
private fun MyRow(itemWidth: Dp, loading: Boolean, it: Int) {
    Box(
        modifier = Modifier
            .size(itemWidth, 100.dp)
            .background(if (loading) Color.Red else Color.Green, RoundedCornerShape(16.dp))
            .padding(16.dp)
    ) {
        Text("Row $it", fontSize = 26.sp, color = Color.White)
    }
}

It takes 1 second for this item to full be ready but if you scroll so how to make this or more items composed?

Actually i'm also looking how to store states of Composables like moveableContentOf does in a Row/Column but it doesn't work with LazyColumn for some reason? It would also another approach to storing Composable state that left composition and show with last state before showing on screen.


Solution

  • One of the ways to compose more offscreen items than one that is composed when scroll starts is increasing LazyColumn/Row viewport size bigger than parent Composable or device dimensions.

    Since, creating dimensions bigger than parent Composable is not permitted by default one option is to use Modifier.layout

    Text("Compose 4 items offscreen in right direction")
    
    LazyRow(
        modifier = Modifier.fillMaxWidth().layout { measurable, constraints ->
            val width = constraints.maxWidth + 4 * itemWidth.roundToPx()
            val wrappedConstraints = constraints.copy(minWidth = width, maxWidth = width)
    
            val placeable = measurable.measure(wrappedConstraints)
    
            layout(
                placeable.width, placeable.height
            ) {
                val xPos = (placeable.width - constraints.maxWidth) / 2
                placeable.placeRelative(xPos, 0)
            }
        },
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(16.dp)
    ) {
        items(30) {
            var loading by remember {
                mutableStateOf(true)
            }
    
            LaunchedEffect(Unit) {
                println("Composing Second LazyRow item: $it")
                delay(1000)
                loading = false
            }
            MyRow(itemWidth, loading, it)
        }
    }
    

    Results 4 more items are composed in right scroll while one while scrolled left.

    enter image description here

    If you wish to make fling gesture, gesture that scrolls after user lifts off finger, to scroll after is slower so items are more like to be loaded. NestedScrollConnection to limit fling speed does the trick. Limiting initial velocity between

    - threshold < available < threshold
    

    So fling gesture is limited in both directions.

    @Composable
    fun rememberFlingNestedScrollConnection() = remember {
        object : NestedScrollConnection {
    
            override suspend fun onPreFling(available: Velocity): Velocity {
                val threshold = 3000f
                val availableX = available.x
                val consumed = if (availableX > threshold) {
                    availableX - threshold
                } else if (availableX < -threshold) {
                    availableX + threshold
                } else {
                    0f
                }
                return Velocity(consumed, 0f)
            }
    
        }
    }
    
      LazyRow(
                modifier = Modifier
                    .nestedScroll(rememberFlingNestedScrollConnection())
     )
    

    enter image description here

    In both cases LazyColumn is set to start position and viewport overflows right side of the screen. But what about making it overflow both sides so it can compose more items in both scroll direction.

    For that positioning 0 to does the trick but you won't be able to see first 4 and last 4 items and to make them visible need to add contentPadding as

    Text("Compose 4 items offscreen in both scroll directions")
    
    LazyRow(
        modifier = Modifier
            .nestedScroll(rememberFlingNestedScrollConnection())
            .fillMaxWidth()
            .layout { measurable, constraints ->
                val width = constraints.maxWidth + 8 * itemWidth.roundToPx()
                val wrappedConstraints = constraints.copy(minWidth = width, maxWidth = width)
    
                val placeable = measurable.measure(wrappedConstraints)
    
                layout(
                    placeable.width, placeable.height
                ) {
                    placeable.placeRelative(0, 0)
                }
            },
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        contentPadding = PaddingValues(
            vertical = 16.dp,
            horizontal = 16.dp + itemWidth * 4
        )
    ) {
        items(30) {
            var loading by remember {
                mutableStateOf(true)
            }
    
            LaunchedEffect(Unit) {
                println("Composing Forth LazyRow item: $it")
                delay(1000)
                loading = false
            }
            MyRow(itemWidth, loading, it)
        }
    }
    

    enter image description here