Scrolling is a fundamental element of any mobile app, and Jetpack Compose provides powerful tools to create smooth and efficient scrolling experiences. This article dives into the world of scroll in Compose, starting with the foundational concepts and gradually progressing towards more complex scenarios.
Compose offers two workhorses for creating scrollable lists: LazyColumn
for vertical scrolling and LazyRow
for horizontal scrolling. They behave similarly to RecyclerView
in XML, efficiently rendering only the visible items while maintaining excellent performance.
Lazy Column
@Composable fun LazyColumnExample() { val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10","Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10") LazyColumn( modifier = Modifier .fillMaxSize() .background(Color.LightGray) ) { items(items.size) { item -> Box( modifier = Modifier .fillMaxWidth() .padding(16.dp), contentAlignment = Alignment.Center ) { Text( text = items.get(item), color = Color.Black ) } } } } @Preview @Composable fun Preview() { LazyColumnExample() }
Lazy Row
@Composable fun LazyRowExample() { val items = listOf("Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10","Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10") LazyRow( modifier = Modifier .fillMaxSize() .background(Color.LightGray) ) { items(items.size) { item -> Box( modifier = Modifier .fillMaxWidth() .padding(16.dp), contentAlignment = Alignment.Center ) { Text( text = items.get(item), color = Color.Black ) } } } } @Preview @Composable fun Preview() { LazyRowExample() }
While LazyColumn
and LazyRow
handle most scrolling needs, ScrollState
offers finer control. It acts as a state holder, keeping track of the current scroll position for various scrollable components like Column
or LazyColumn
.
Understanding Scroll State
In Jetpack Compose, ScrollState
is a state holder that keeps track of the current scroll position for scrollable components such as Column
, LazyColumn
, or other containers that support scrolling. ScrollState
gives us:
- Position Tracking: You can use
ScrollState
to access the current scroll offset or position of a scrollable component. - Smooth Scrolling:
ScrollState
allows you to control smooth scrolling to specific positions in a list. - Listening to Scroll Events: You can observe changes in the scroll position, which is particularly useful for things like showing/hiding toolbar animations based on scroll offset.
Important Properties and Methods of ScrollState
Properties:
value
: The current scroll offset in pixels.maxValue
: The maximum scroll offset. This is helpful for detecting when the scroll has reached the end of a container.
Methods:
animateScrollTo(offset: Int)
: Smoothly animates scrolling to the given offset in pixels.scrollTo(offset: Int)
: Instantly scrolls to the given offset.
Scroll State Types in Compose
There are different types of scroll states depending on the type of container:
- ScrollState: Used for simple scrolling in containers like
Column
. - LazyListState: Specifically used for
LazyColumn
andLazyRow
, giving more control over items and visibility states.
Example 1: Using ScrollState with Column
To start, let’s see a simple example where we use ScrollState
to observe and control the scroll position of a Column
that supports vertical scrolling.
import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @Composable fun ScrollableColumnExample() { // Initialize the scroll state val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) // Attach scroll state to Column ) { // Display some items with varying colors for (i in 1..50) { Box( modifier = Modifier .fillMaxWidth() .height(100.dp) .background(if (i % 2 == 0) Color.LightGray else Color.Gray), contentAlignment = Alignment.Center ) { Text("Item $i") } } } // Observe the scroll offset and print it LaunchedEffect(scrollState.value) { println("Current scroll position: ${scrollState.value}") } }
Explanation
In this example:
- We create a
Column
with aScrollState
that allows it to scroll vertically. verticalScroll(scrollState)
attaches the scroll state to the column.- Inside the
LaunchedEffect
, we print the current scroll position each timescrollState.value
changes.
This example demonstrates basic scroll behavior in a Column
and how to observe the scroll position.
Example 2: Smooth Scrolling with ScrollState
If you want to programmatically scroll to a specific position, you can use scrollState.animateScrollTo(offset)
. This is helpful for features like “scroll to top” or “scroll to a specific item.”
import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @Composable fun SmoothScrollingExample() { val scrollState = rememberScrollState() val coroutineScope = rememberCoroutineScope() Column(modifier = Modifier.fillMaxSize()) { Button( onClick = { // Smooth scroll to the top coroutineScope.launch { scrollState.animateScrollTo(0) } }, modifier = Modifier.fillMaxWidth() ) { Text("Scroll to Top") } Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) ) { for (i in 1..50) { Text( text = "Item $i", modifier = Modifier .fillMaxWidth() .padding(16.dp), color = Color.White ) } } } }
Explanation
- We use
rememberCoroutineScope()
to launch a coroutine that allows asynchronous scrolling. - The button calls
scrollState.animateScrollTo(0)
to scroll smoothly to the top of the list. animateScrollTo()
is an asynchronous function, making the scrolling smooth and animated.
Job Offers
Nested Scrolling
Nested scrolling is a concept where multiple scrolling containers work together to create a single scroll gesture.
Compose provides multiple ways of handling nested scrolling between composables. A typical example of nested scrolling is a list inside another list, and a more complex case is a collapsing toolbar.
Let’s understand the basic nested scrolling with an example.
Here we have a scrollable list, and each list has a child list which is also scrollable. we are also adding expand and collapse view to show and hide each list item’s child list.
import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable fun NestedScrollingExample() { // Parent scroll state val parentScrollState = rememberScrollState() // Sample list data val items = (1..10).toList() Column( modifier = Modifier .fillMaxSize() .verticalScroll(parentScrollState) .padding(16.dp) ) { items.forEach { item -> ExpandableItem(item) } } } @Composable fun ExpandableItem(item: Int) { // State to track if the item is expanded var isExpanded by remember { mutableStateOf(false) } Column( modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp) .background(Color.LightGray) ) { // Header for the expandable item Row( modifier = Modifier .fillMaxWidth() .clickable { isExpanded = !isExpanded } .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = "Item $item", fontSize = 18.sp, modifier = Modifier.weight(1f) ) Icon( imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, contentDescription = "Expand/Collapse" ) } // Child scrollable list, visible only when expanded if (isExpanded) { val childScrollState = rememberScrollState() Column( modifier = Modifier .fillMaxWidth() .height(150.dp) // Fixed height for nested scrollable area .verticalScroll(childScrollState) .background(Color.White) .padding(8.dp) ) { // Nested list content (1..5).forEach { subItem -> Text( text = "Sub-item $subItem of Item $item", fontSize = 16.sp, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) .background(Color(0xFFF0F0F0)) .padding(8.dp) ) } } } } } @Preview @Composable fun showPeview() { NestedScrollingExample() }
How Nested Scrolling Works Here
- The parent scroll (
parentScrollState
) allows the entire list of items to scroll vertically. - Each child scroll (
childScrollState
) manages the scrolling within the expanded item independently. - This approach avoids using
LazyColumn
orLazyRow
, handling scrolling manually withScrollState
instead.
Up Next: Conquering Collapsing Toolbars
This article has covered the fundamental aspects of scroll in Compose. In the next part, we’ll tackle a more intricate scenario: creating a collapsing toolbar that reacts to scrolling within a list. We’ll explore how to leverage nested scrolling to achieve this dynamic and visually appealing effect.
This concludes the first part of our series on scroll in Jetpack Compose. Stay tuned for the next chapter where we’ll unlock the secrets of collapsing toolbars and complex nested scrolling!
Now PART 2 is available
References
I hope this article was helpful to you. You can write me back at karishma.agr1996@gmail.com if you want me to improve something in upcoming articles. Your feedback is valuable.
Also, follow me on Medium and Linkedin
Your claps are appreciated to help others find this article 😃 .
This article is previously published on proandroiddev.com.