Sign in
Topics
Build Android apps with prompts or Figma
Why does
Modifier.scrollable()
in Jetpack Compose not move your layout? This blog breaks down how scrolling actually works under the hood and why the modifier behaves differently than expected. It also provides clear fixes with examples so you can make your UI scroll smoothly.
Building with Jetpack Compose initially feels simple. Then you add Modifier.scrollable
to a layout, and nothing moves. The screen stays fixed. Frustration sets in.
Why does the modifier.scrollable()
not scroll in Jetpack Compose?
The issue comes from how scrolling works under the hood. The modifier is not broken, but it behaves differently than expected.
This blog explains the reason, shows clear fixes, and walks through examples. By the end, you will know how to make it work and why the problem happens.
The scrollable
modifier by itself does not make your Column
or Box
scroll. It only provides low-level scrolling behavior. That means you need to define a scroll state, orientation, and connect it with child composables. Without these, the UI remains static, regardless of the number of items added. Developers expect default behavior, but Compose forces you to declare it explicitly.
Press enter analogy: You can press enter on the keyboard, but without wiring it to the ignition, the engine won't turn over. This shows that scrollable exists, but does nothing without proper connections.
Disconnected button: Similarly, adding scrollable
without the state is like pressing a button that is not connected. The modifier responds but cannot take effect.
Function without control: The function exists, but it has no control over your layout. A function without state or direction cannot produce meaningful output.
Developer expectations: This misunderstanding is one of the main reasons developers get stuck. Developers expect default behavior, but Compose makes you define it explicitly.
Understanding the difference between scrollable
and scroll state
is the first step toward fixing the issue. Jetpack Compose separates low-level modifiers from higher-level helpers, and that often confuses new developers. Let’s break this down before showing you code.
Raw control: Using scrollable
alone is like writing boilerplate code without leveraging built-in helpers. It gives you freedom, but also more work.
Shortcuts with helpers: verticalScroll
and horizontalScroll
automatically handle orientation and state management. These are shortcuts for common use cases.
Choosing correctly: Choose the right modifier based on your content size and layout needs. The wrong one can result in performance issues.
Mistakes in selection: Failing to pick the correct approach often results in static screens. Many developers report that layouts do not move due to this mistake.
Component | Purpose | Usage |
---|---|---|
Modifier.scrollable | Provides scroll interaction but requires state and orientation | Low-level control |
Modifier.verticalScroll | Higher-level modifier that internally uses scrollable with vertical orientation | Most common case |
Modifier.horizontalScroll | Same as above but horizontal | Used for carousels or horizontal content |
Here’s the relationship between scrollable, state, and layout.
This flow shows:
Many developers encounter recurring issues when using Modifier.scrollable
. Each mistake usually ties back to state handling, gesture interception, or configuration. Here’s a structured list of the most frequent pitfalls and their fixes.
Forgetting to remember state: Always use rememberScrollState()
. Not remembering state creates a new object every recomposition, causing janky behavior.
Applying scrollable directly: Use verticalScroll
or horizontalScroll
for standard behavior. Direct scrollable is too low-level for everyday use.
Mixing with LazyColumn: Avoid nesting scrollable layouts. Nested scrolls often conflict and fail to function properly, resulting in poor user interaction.
Clickable intercepting gestures: Sequence modifiers carefully. Place scroll modifiers before clickable for smoother interaction and fewer gesture conflicts.
Enabled flag set to false: Check enabled
parameter. False disables the entire scroll even if everything else is correct.
1@Composable 2fun ScrollableColumnExample() { 3 val scrollState = rememberScrollState() 4 Column( 5 modifier = Modifier 6 .fillMaxSize() 7 .verticalScroll(scrollState) 8 ) { 9 repeat(50) { 10 Text("Item #$it", modifier = Modifier.padding(16.dp)) 11 } 12 } 13} 14
Modifiers like scrollable
have an enabled
parameter that can dramatically affect runtime behavior. It is small but powerful, and often overlooked by beginners.
False disables scroll: If false
, the scroll is disabled. This is useful when you want to block user interaction during runtime updates temporarily.
Static UI: UI remains static even with many items. Developers often mistake this for a bug and waste time debugging.
Verify enabled state: Verify you are not unintentionally disabling scroll. Always review modifier parameters carefully in your composable functions.
Common oversight: Small mistakes here often appear as larger layout issues. Debugging can become confusing without checking enabled.
Choosing between Column
with scroll or LazyColumn
is a common question in UI development. Your choice depends on performance requirements, dataset size, and type of composable elements.
Column with verticalScroll: Use Column
+ verticalScroll
for small sets of elements. This is light, declarative, and simple for UI development.
LazyColumn for performance: Use LazyColumn
for large or infinite lists. It saves resources by rendering only visible items at runtime.
Column performance issue: Columns with thousands of children can hurt performance. This leads to memory pressure and frame drops.
LazyColumn advantage: LazyColumn recycles child composables, keeping UI smooth. This approach is ideal for conversations, messages, and feeds.
1@Composable 2fun LazyColumnExample() { 3 LazyColumn { 4 items(1000) { index -> 5 Text("Message #$index", modifier = Modifier.padding(16.dp)) 6 } 7 } 8} 9
Scroll can also fail when your layout hierarchy or shapes block gestures. Understanding how these elements interact is critical for debugging.
Box layers blocking scroll: Overlapping Box layers with null size can block scroll. Even invisible widgets can intercept gestures unknowingly.
Wrong orientation: Wrong orientation in scrollable stops movement. Always match layout direction with the intended scroll axis.
Full-size images: Full-size images need proper layout space. Otherwise, they consume space without enabling scroll.
Shape clipping: Shape clipping may reduce gesture area, affecting scroll. Rounded or clipped shapes shrink the touch zone.
The scrollable
modifier provides additional parameters like orientation, reverseDirection, and lambdas for gesture handling. These are valuable for advanced scenarios in composable functions.
Define orientation and reverse: Define orientation and reverse direction. This allows building creative UI elements like carousels.
Gesture interception: Intercept gestures with lambda functions. These lambdas give more runtime control over how gestures propagate.
Custom UI cases: Useful for complex UI and custom scroll behaviors. Particularly effective for maps, image galleries, and widgets.
Combining elements: Lets you mix multiple scrollable and interactive elements. This unlocks advanced design patterns in declarative programming.
1@Composable 2fun CustomScrollableBox() { 3 val scrollState = rememberScrollState() 4 Box( 5 modifier = Modifier 6 .size(200.dp) 7 .scrollable( 8 state = scrollState, 9 orientation = Orientation.Vertical, 10 enabled = true 11 ) 12 .background(Color.LightGray) 13 ) { 14 Column { 15 repeat(20) { 16 Text("Element #$it", modifier = Modifier.padding(12.dp)) 17 } 18 } 19 } 20} 21
When scroll doesn’t work, debugging systematically saves time. Many runtime issues are caused by oversight in state, gestures, or composable parameters.
Print scroll values: Print scroll values using LaunchedEffect
. Helps verify if state updates correctly across recompositions.
Log parameters: Log orientation and parameters. Logs reveal wrong values quickly, especially when testing.
Disable clickable temporarily: Temporarily disable clickable to isolate issues. This ensures clicks are not stealing scroll gestures.
Check child composables: Check nested child composables. Sometimes children intercept gestures unknowingly, especially in complex layouts.
Verify gesture area: Verify gesture area and parent size. Parent containers define available interaction zones at runtime.
Further Reading & Community Help
Stack Overflow discussion
“Modifier.scrollable doesn’t scroll the content ”
– A developer asks why a
scrollable layout
doesn’t scroll, and gets a reply explaining the need for a remembered state or to useverticalScroll
instead.
To enhance your learning and ensure a smooth UI development experience in Jetpack Compose, explore the appropriate tools and resources. These not only save development time but also help in verifying behavior across different environments.
Resources: Use official Android documentation and Compose samples. These resources act as reference points for best practices.
Tools: Profilers, layout inspectors, and debugging tools in Android Studio provide runtime insights. They highlight null values, parameters, and gesture points.
Widgets: Compose offers many built-in widgets. Experimenting with them helps you understand composable functions better.
Map and conversation examples: Building interactive maps or chat conversations can highlight how scroll, gestures, and state management integrate.
Account and sign flows: Many apps require account creation and sign-in screens. Scroll handling in these layouts is critical for usability.
If you love building UI but hate writing too much boilerplate code, try Rocket.new . Build any app with simple prompts, no code required. Just write what you expect, and let Rocket create it. Designing interfaces can be fast and creative.
Remember to define state using rememberScrollState()
. Check clickable modifiers do not block gestures. Verify enabled
parameter is true. With these adjustments, scroll works smoothly across your layouts. Following these makes debugging faster and avoids unnecessary frustration.
When working with larger datasets, switch to LazyColumn
instead of a simple scrollable Column. Understanding these trade-offs will help you create reliable layouts, interactive conversations, and responsive UI. With the right tools, resources, and declarative programming approach, Jetpack Compose makes scroll handling straightforward and powerful.