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
.
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.
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.
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.
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.